链表高级操作与算法

链表是数据结构中的基础,但也是面试和实际开发中的重点考察对象。今天我们将深入探讨链表的高级操作和常见算法,让你能够轻松应对各种链表问题。

1. 链表翻转 - 最经典的链表问题

链表翻转是面试中的常见题目,也是理解链表指针操作的绝佳练习。

1.1 迭代方法实现

ListNode* reverseList(ListNode* head) {ListNode* prev = nullptr;ListNode* curr = head;while (curr != nullptr) {ListNode* nextTemp = curr->next; // 暂存下一个节点curr->next = prev;              // 反转指针prev = curr;                    // prev 前进curr = nextTemp;                // curr 前进}return prev; // 新的头节点}

这种方法就像是在倒一叠书:你需要一本一本地翻转,过程中需要记住当前的书、前一本书和下一本书的位置。时间复杂度为 O(n),空间复杂度为 O(1)。

1.2 递归方法实现

ListNode* reverseList(ListNode* head) {// 基本情况:空链表或只有一个节点if (head == nullptr || head->next == nullptr) {return head;}// 递归反转剩余部分ListNode* newHead = reverseList(head->next);// 改变指针方向head->next->next = head;head->next = nullptr;return newHead;}

递归方法更像是魔法,它先抵达链表尾部,然后在"归"的过程中一个接一个地反转指针。这就像是我们先走到队列末尾,然后从末尾开始依次让每个人面朝相反方向。

2. 检测环形链表

在许多实际应用中,确定链表是否存在环(循环)非常重要,因为环会导致无限循环。

2.1 快慢指针法

bool hasCycle(ListNode *head) {if (!head || !head->next) return false;ListNode *slow = head;ListNode *fast = head;while (fast && fast->next) {slow = slow->next;      // 慢指针每次走一步fast = fast->next->next; // 快指针每次走两步if (slow == fast) return true; // 相遇则存在环}return false; // 如果fast到达NULL,则无环}

这就像操场上跑步的两个人:一个跑得快,一个跑得慢。如果跑道是环形的,快的人最终会从后面追上慢的人;如果跑道是直线,快的人会先到终点。

2.2 找到环的入口点

ListNode *detectCycle(ListNode *head) {if (!head || !head->next) return nullptr;ListNode *slow = head;ListNode *fast = head;bool hasCycle = false;// 检测是否有环while (fast && fast->next) {slow = slow->next;fast = fast->next->next;if (slow == fast) {hasCycle = true;break;}}if (!hasCycle) return nullptr;// 找到环的入口点slow = head;while (slow != fast) {slow = slow->next;fast = fast->next;}return slow; // 环的入口点}

这个算法使用了一个有趣的数学结论:当快慢指针相遇后,将慢指针重置到链表头,然后两个指针以相同的速度前进,它们会在环的入口处相遇。这就像两个人在环形操场不同位置出发,经过一定圈数后在某个特定点相遇。

3. 找到链表的中间节点

找到链表的中间节点对于很多算法都是关键一步,比如排序或二分查找。


ListNode* middleNode(ListNode* head) {ListNode* slow = head;ListNode* fast = head;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;}return slow;}

这个技巧也利用了快慢指针。想象你和朋友沿着一条路走,朋友的速度是你的两倍。当朋友到达终点时,你恰好在中间位置。如果链表长度为奇数,返回的是正中间的节点;如果为偶数,则返回的是中间偏右的节点。

4. 合并两个有序链表

将两个已排序的链表合并成一个新的排序链表是另一个常见问题。


ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {// 创建哑节点作为合并链表的头ListNode dummy(0);ListNode* tail = &dummy;while (l1 && l2) {if (l1->val < l2->val) {tail->next = l1;l1 = l1->next;} else {tail->next = l2;l2 = l2->next;}tail = tail->next;}// 连接剩余部分tail->next = l1 ? l1 : l2;return dummy.next;}

这就像合并两队排好队的人,每次从两队的队头选择较小的一个人加入新队伍。

5. 判断回文链表

回文是指从前向后和从后向前读都相同的序列。判断一个链表是否为回文链表是一个有趣的挑战。

bool isPalindrome(ListNode* head) {if (!head || !head->next) return true;// 找到中间节点ListNode* slow = head;ListNode* fast = head;while (fast->next && fast->next->next) {slow = slow->next;fast = fast->next->next;}// 反转后半部分ListNode* secondHalf = reverseList(slow->next);// 比较前半部分和反转后的后半部分ListNode* p1 = head;ListNode* p2 = secondHalf;bool result = true;while (p2) {if (p1->val != p2->val) {result = false;break;}p1 = p1->next;p2 = p2->next;}// 恢复链表原状(可选)slow->next = reverseList(secondHalf);return result;}

这个算法的思路是:先找到链表的中点,然后反转后半部分,最后从两端向中间比较。这就像检查一个单词是否为回文:我们可以从两端同时读取并比较。

6. 删除链表中的倒数第N个节点

这是一道考察链表遍历技巧的经典题目。

ListNode* removeNthFromEnd(ListNode* head, int n) {ListNode dummy(0);dummy.next = head;ListNode* first = &dummy;ListNode* second = &dummy;// 第一个指针先前进 n+1 步for (int i = 0; i <= n; i++) {first = first->next;}// 两个指针一起前进,直到第一个指针到达末尾while (first) {first = first->next;second = second->next;}// 删除倒数第 n 个节点ListNode* toDelete = second->next;second->next = second->next->next;delete toDelete;return dummy.next;}

这个技巧使用了两个指针,两者之间保持固定距离(n+1)。当第一个指针到达链表末尾时,第二个指针恰好指向倒数第 n+1 个节点,这样我们就可以删除倒数第 n 个节点了。这就像一列行进的士兵,当排头到达终点时,排尾的位置也是确定的。

7. 划分链表 - 奇偶节点分离

将链表按照奇偶位置划分,先奇数位置的节点,再偶数位置的节点。

ListNode* oddEvenList(ListNode* head) {if (!head || !head->next) return head;ListNode* odd = head;           // 奇数节点ListNode* even = head->next;    // 偶数节点ListNode* evenHead = even;      // 保存偶数链表的头while (even && even->next) {odd->next = even->next;     // 连接奇数节点odd = odd->next;even->next = odd->next;     // 连接偶数节点even = even->next;}odd->next = evenHead;           // 连接奇偶两个链表return head;}

这个算法将链表分成两部分:奇数位置节点和偶数位置节点,然后将偶数链表接在奇数链表后面。它就像是把队伍中的人按单双号分成两队,然后再把第二队排在第一队后面。

8. 复杂链表的复制

一个复杂链表,其中每个节点除了有一个 next 指针外,还有一个 random 指针,随机指向链表中的任意节点或 NULL。复制这样的链表是一个挑战。

Node* copyRandomList(Node* head) {if (!head) return nullptr;// 第一步:在每个原始节点后创建一个新节点Node* curr = head;while (curr) {Node* copy = new Node(curr->val);copy->next = curr->next;curr->next = copy;curr = copy->next;}// 第二步:处理random指针curr = head;while (curr) {if (curr->random) {curr->next->random = curr->random->next;}curr = curr->next->next;}// 第三步:分离两个链表Node dummy(0);Node* newTail = &dummy;curr = head;while (curr) {newTail->next = curr->next;newTail = newTail->next;curr->next = curr->next->next;curr = curr->next;}return dummy.next;}

这个巧妙的算法分三步:首先,在每个原始节点后创建其复制节点;然后,利用这种交替的结构设置random指针;最后,分离两个链表。这就像是为一组人创建克隆体,每个克隆体站在原人后面,然后根据原有的社交关系建立克隆体之间的联系,最后将克隆体组成新的队伍。

9. 实际应用案例

9.1 LRU (最近最少使用) 缓存

LRU 缓存是一种常见的缓存淘汰策略,可以用链表实现。

class LRUCache {private:int capacity;list<pair<int, int>> cache; // key-value对的链表unordered_map<int, list<pair<int, int>>::iterator> map; // 哈希表,快速找到key在链表中的位置public:LRUCache(int capacity) : capacity(capacity) {}int get(int key) {auto it = map.find(key);if (it == map.end()) return -1;// 将访问的节点移到链表前端cache.splice(cache.begin(), cache, it->second);return it->second->second;}void put(int key, int value) {auto it = map.find(key);if (it != map.end()) {// 更新已存在的keyit->second->second = value;cache.splice(cache.begin(), cache, it->second);return;}// 缓存已满,删除最久未使用的元素if (cache.size() == capacity) {int oldKey = cache.back().first;cache.pop_back();map.erase(oldKey);}// 插入新元素到前端cache.emplace_front(key, value);map[key] = cache.begin();}};

在这个实现中,我们使用双向链表保存键值对,最近使用的在前,最久未使用的在后。哈希表用于O(1)时间内找到链表中的节点。这个例子展示了如何将链表和哈希表结合使用,实现高效的缓存机制。

9.2 多项式表示

链表可以用来表示多项式,每个节点代表一项,包含系数和指数。


struct PolyNode {int coef;  // 系数int exp;   // 指数PolyNode* next;PolyNode(int c, int e) : coef(c), exp(e), next(nullptr) {}};// 两个多项式相加PolyNode* addPoly(PolyNode* poly1, PolyNode* poly2) {PolyNode dummy(0, 0);PolyNode* tail = &dummy;while (poly1 && poly2) {if (poly1->exp > poly2->exp) {tail->next = new PolyNode(poly1->coef, poly1->exp);poly1 = poly1->next;} else if (poly1->exp < poly2->exp) {tail->next = new PolyNode(poly2->coef, poly2->exp);poly2 = poly2->next;} else {int sumCoef = poly1->coef + poly2->coef;if (sumCoef != 0) {tail->next = new PolyNode(sumCoef, poly1->exp);}poly1 = poly1->next;poly2 = poly2->next;}if (tail->next) tail = tail->next;}// 处理剩余项tail->next = poly1 ? poly1 : poly2;return dummy.next;}

这个例子展示了如何使用链表表示和操作多项式,是链表在代数计算中的一个实际应用。

10. 性能优化与实践建议

  1. 避免频繁分配/释放内存:在处理大量链表操作时,考虑使用内存池或节点缓存来减少内存分配的开销。
  1. 使用哑节点简化代码:在处理链表头部可能变化的情况时,使用哑节点(dummy node)可以统一处理流程,避免特殊情况。
  1. 理解并灵活运用快慢指针:快慢指针是链表操作的利器,掌握它可以解决大量问题,如检测环、找中点等。
  1. 注意指针操作顺序:在修改链表结构时,务必注意指针操作的顺序,避免丢失节点引用。
  1. 学会利用递归思想:某些链表问题用递归解决会更加简洁优雅,如反转链表、合并有序链表等。

总结

链表作为一种基础数据结构,其灵活性和多变性使得它在许多场景下都有应用。通过掌握本文介绍的高级操作和算法,你将能够应对大部分链表相关的编程挑战。

记住,链表的精髓在于理解和操作指针。只要你掌握了这一点,再复杂的链表问题也能迎刃而解。希望这篇文章能帮助你更深入地理解和应用链表这一重要的数据结构!

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

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

相关文章

架构思维:构建高并发读服务_使用懒加载架构实现高性能读服务

文章目录 一、引言二、读服务的功能性需求三、两大基本设计原则1. 架构尽量不要分层2. 代码尽可能简单 四、实战方案&#xff1a;懒加载架构及其四大挑战五、改进思路六、总结与思考题 一、引言 在任何后台系统设计中&#xff0c;「读多写少」的业务场景占据主流&#xff1a;浏…

在运行 Hadoop 作业时,遇到“No such file or directory”,如何在windows里打包在虚拟机里运行

最近在学习Hadoop集群map reduce分布运算过程中&#xff0c;经多方面排查可能是电脑本身配置的原因导致每次运行都会报“No such file or directory”的错误&#xff0c;最后我是通过打包文件到虚拟机里运行得到结果&#xff0c;具体步骤如下&#xff1a; 前提是要保证maven已经…

软考-软件设计师中级备考 11、计算机网络

1、计算机网络的分类 按分布范围分类 局域网&#xff08;LAN&#xff09;&#xff1a;覆盖范围通常在几百米到几千米以内&#xff0c;一般用于连接一个建筑物内或一个园区内的计算机设备&#xff0c;如学校的校园网、企业的办公楼网络等。其特点是传输速率高、延迟低、误码率低…

【C#】.net core6.0无法访问到控制器方法,直接404。由于自己的不仔细,出现个低级错误,这让DeepSeek看出来了,是什么错误呢,来瞧瞧

&#x1f339;欢迎来到《小5讲堂》&#x1f339; &#x1f339;这是《C#》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。&#x1f339; &#x1f339;温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01;&#…

当LLM遇上Agent:AI三大流派的“复仇者联盟”

你一定听说过ChatGPT和DeepSeek&#xff0c;也知道它们背后的LLM&#xff08;大语言模型&#xff09;有多牛——能写诗、写代码、甚至假装人类。但如果你以为这就是AI的极限&#xff0c;那你就too young too simple了&#xff01; 最近&#xff0c;**Agent&#xff08;智能体&a…

Spring Boot多模块划分设计

在Spring Boot多模块项目中&#xff0c;模块划分主要有两种思路&#xff1a;​​技术分层划分​​和​​业务功能划分​​。两种方式各有优缺点&#xff0c;需要根据项目规模、团队结构和业务特点来选择。 ​​1. 技术分层划分&#xff08;横向拆分&#xff09;​​ 结构示例&…

两次解析格式化字符串 + 使用SQLAlchemy的relationship执行任意命令 -- link-shortener b01lersCTF 2025

题目描述: A fast and reliable link shortener service, with a new feature to add private links! 我们走一遍逻辑 注册 app.route("/register", methods[GET, POST]) def register(): """ 用户注册路由&#xff0c;处理用户注册请求&#xff…

后端id类型为long类型时,返回给前端浏览器四舍五入,导致id精度缺失问题

背景 今天在代码里&#xff0c;掉了别人写的接口&#xff0c;有个id的字段是long类型的&#xff0c;我这边加点参数返回给前端&#xff0c;然后前端根据id修改&#xff0c;结果修改的数据记录有&#xff0c;但是没起作用&#xff0c;后来发现根据他传给我的id在后台数据库查不…

Scartch038(四季变换)

知识回顾 1.了解和简单使用音乐和视频侦测模块 2.使用克隆体做出波纹特效 3.取色器妙用侦测背景颜色 前言 我国幅员辽阔,不同地方的四季会有不同的美丽景色,这节课我带你使用程序做一个体现北方四季变化的程序 之前的程序基本都是好玩的,这节课做一个能够赏心悦目的程序。…

JVM happens-before 原则有哪些?

理解Java Memory Model (JMM) 中的 happens-before 原则对于编写并发程序有很大帮助。 Happens-before 关系是 JMM 用来描述两个操作之间的内存可见性以及执行顺序的抽象概念。如果一个操作 A happens-before 另一个操作 B (记作 A hb B)&#xff0c;那么 JMM 向你保证&#x…

从 Eclipse Papyrus / XText 转向.NET —— SCADE MBD技术的演化

从KPN[1]的萌芽开始&#xff0c;到SCADE的推出[2]&#xff0c;再到Scade 6的技术更迭[3]&#xff0c;SCADE 基于模型的开发技术已经历许多。现在&#xff0c;Scade One 已开启全新的探索 —— 从 Eclipse Papyrus / XText 转向.NET 8跨平台应用。 [1]: KPN, Kahn进程网络 (197…

osquery在网络安全入侵场景中的应用实战(二)

背景 上次写了osquery在网络安全入侵场景中的应用实战(一)结果还不错,这次篇目二再增加一些场景。osquery主要解决的时员工被入侵之后电脑该如何溯源取证的问题。通常EDR会有日志,但是不会上报全量的日志。发现机器有恶意文件需要上级取证的时候,往往是比较麻烦的,会有这…

opencv+opencv_contrib+cuda和VS2022编译

本文介绍使用OpenCV和OpenCV_Contrib源码及Cuda进行编译的过程&#xff0c;编译过程中会用到OpenCV、OpenCV_Contrib、Toolkit、Cmake、VS2022等工具&#xff0c;最终编译OpenCV的Cuda版本。 一、OpenCV下载地址 OpenCV官网下载地址:https://opencv.org/releases/#&#xff0…

spring中的@ConfigurationProperties注解详解

一、核心功能与作用 ConfigurationProperties 是Spring Boot中用于将外部配置&#xff08;如application.properties或application.yml中的属性&#xff09;绑定到Java对象的核心注解。其核心功能包括&#xff1a; 配置集中管理&#xff1a;将分散的配置属性按前缀绑定到Java类…

【C/C++】函数模板

&#x1f3af; C 学习笔记&#xff1a;函数模板&#xff08;Function Template&#xff09; 本文是面向 C 初学者的函数模板学习笔记&#xff0c;内容包括基本概念、定义与使用、实例化过程、注意事项等&#xff0c;附带示例代码&#xff0c;便于理解与复现。 &#x1f4cc; 一…

电子病历高质量语料库构建方法与架构项目(智能数据目录篇)

电子病历高质量语料库的构建是医疗人工智能发展的基础性工作,而智能数据目录作为数据治理的核心组件,能够有效管理这些语料资源。本文将系统阐述电子病历高质量语料库的构建方法与架构,特别聚焦于智能数据目录的设计与实现,包括数据目录的功能定位、元数据管理、构建步骤以…

前端懒加载(Lazy Loading)实战指南

&#x1f680; 前端懒加载&#xff08;Lazy Loading&#xff09;实战指南 懒加载是现代 Web 性能优化的“常规操作”。它的目标简单直接&#xff1a;让用户只加载“当下真正需要的资源”。从静态资源、组件、模块到数据&#xff0c;每一层都可以使用懒加载技术&#xff0c;构建…

在 Ubuntu 系统中,查看已安装程序的方法

在 Ubuntu 系统中&#xff0c;查看已安装程序的方法取决于软件的安装方式&#xff08;如通过 apt、snap、flatpak 或手动安装&#xff09;。以下是几种常见方法&#xff1a; 通过 apt 包管理器安装的软件 适用于通过 apt 或 dpkg 安装的 .deb 包。 列出所有已安装的软件包&…

性能优化实践:性能监控体系

性能优化实践&#xff1a;性能监控体系 在Flutter应用开发中&#xff0c;建立一个完善的性能监控体系对于保证应用质量和用户体验至关重要。本文将从实战角度深入讲解如何搭建Flutter应用的性能监控体系&#xff0c;包括监控指标的设计、数据采集实现、分析平台搭建等内容。 …

kotlin 02flow-sharedFlow 完整教程

一 sharedFlow是什么 SharedFlow 是 Kotlin 协程中 Flow 的一种 热流&#xff08;Hot Flow&#xff09;&#xff0c;用于在多个订阅者之间 共享事件或数据流。它适合处理 一次性事件&#xff08;如导航、弹窗、Toast、刷新通知等&#xff09;&#xff0c;而不是持续状态。 ✅ …