链表经典OJ问题详解

链表经典OJ问题详解

文章目录

前言

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

2. 反转一个单链表

3. 链表的中间结点

4. 链表中倒数第k个结点

5. 合并两个有序链表

6. 链表分割

7. 链表的回文结构

8. 相交链表

9. 判断链表中是否有环

10. 返回链表开始入环的第一个结点

结语


前言

链表是一种基础且重要的数据结构,在程序设计中有着广泛的应用。由于其物理存储的非连续性,链表在插入、删除操作上具有独特的优势,但也给某些操作(如随机访问)带来了挑战。在技术面试和算法竞赛中,链表相关的题目出现频率极高,熟练掌握链表的常见操作和经典问题的解法,是每个程序员必备的技能。本文精选了10道经典的链表OJ题目,从思路分析到C语言代码实现,逐步详解,并穿插了快慢指针、哑结点等常用技巧的讲解,每道题目都附带了对应的在线练习链接,方便读者动手实践。希望能帮助读者深入理解链表,轻松应对各类链表问题。

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

题目描述
删除链表中所有值为 val 的结点,返回删除后的链表头结点。🔗 力扣 203. 移除链表元素 

思路分析
创建一个带哨兵位的新链表,将符合条件的节点接到新链表后面,然后返回哨兵位的next节点,注意尾节点的next需要置为NULL,如果原链表是空链表,那么直接返回空链表。

代码实现

typedef struct ListNode ListNode; struct ListNode* removeElements(struct ListNode* head, int val) { if(head==NULL) { return NULL; } ListNode* newhead,* newtail; newhead=newtail=(ListNode*)malloc(sizeof(ListNode)); ListNode* pcur=head; while(pcur) { if(pcur->val!=val) { newtail->next=pcur; newtail=newtail->next; } pcur=pcur->next; } newtail->next=NULL; return newhead->next; }

2. 反转一个单链表

题目描述
反转一个单链表,返回反转后的链表头结点。🔗 力扣 206. 反转链表 

思路分析
迭代法:使用三个指针 prevcurrnext。初始时 prev 为 NULLcurr 指向头结点。每次将 curr->next 指向 prev,然后三个指针整体后移,直到 curr 为 NULL,此时 prev 即为新头结点。

代码实现

typedef struct ListNode ListNode; struct ListNode* reverseList(struct ListNode* head) { ListNode* n1,*n2,*n3; if(head==NULL) return head; n1=NULL,n2=head,n3=head->next; while(n2) { n2->next=n1; n1=n2; n2=n3; if(n3) n3=n3->next; } return n1; }

3. 链表的中间结点

题目描述
给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。🔗 力扣 876. 链表的中间结点

思路分析
快慢指针法:定义两个指针 slow 和 fast,初始都指向头结点。slow 每次走一步,fast 每次走两步。当 fast 到达链表末尾时,slow 恰好指向中间结点。若链表长度为偶数,fast 为空时 slow 指向第二个中间结点。

代码实现

typedef struct ListNode ListNode; struct ListNode* middleNode(struct ListNode* head) { /*int count=0; if(head==NULL) return head; ListNode* pcur=head; while(pcur) { pcur=pcur->next; count++; } pcur=head; for(int i=0;i<count/2;i++) { pcur=pcur->next; } return pcur; */ ListNode* slow,*fast; slow=fast=head; while(fast&&fast->next)//这里注意顺序不能更改 { fast=fast->next->next; slow=slow->next; } return slow; }

4. 链表中倒数第k个结点

题目描述
输入一个链表,输出该链表中倒数第k个结点。🔗 力扣 面试题 22. 链表中倒数第k个节点

思路分析
双指针法:定义两个指针 fast 和 slow,初始都指向头结点。先让 fast 指针向前移动 k 步,然后 fast 和 slow 同时移动。当 fast 到达链表末尾时,slow 恰好指向倒数第k个结点。需要注意 k 大于链表长度的情况。

代码实现

typedef struct ListNode Listnode; struct ListNode* trainingPlan(struct ListNode* head, int cnt) { Listnode* fast=head,* slow=head; if(head==NULL) return NULL; while(cnt--) { fast=fast->next; } while(fast) { fast=fast->next; slow=slow->next; } return slow; }

5. 合并两个有序链表

题目描述
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的。🔗 力扣 21. 合并两个有序链表 

思路分析
迭代法:创建一个虚拟头结点 dummy,用 tail 指针指向新链表的尾部。首先判断两个链表中有无空链表,返回其中非空链表,然后比较两个链表当前结点的值,将较小的结点接到 tail 后面,并移动对应链表的指针。当其中一个链表遍历完后,将另一个链表的剩余部分直接接上。

代码实现

typedef struct ListNode ListNode; struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) { ListNode* l1 = list1; ListNode* l2 = list2; ListNode* newhead = NULL; ListNode* newtail = NULL; // 处理空链表情况 if (l1 == NULL) return l2; if (l2 == NULL) return l1; // 确定第一个节点(初始化 newhead 和 newtail) if (l1->val < l2->val) { newhead = newtail = l1; l1 = l1->next; } else { newhead = newtail = l2; l2 = l2->next; } while (l1 && l2) { if (l1->val < l2->val) { newtail->next = l1; // 将 l1 节点挂到尾部 l1 = l1->next; // l1 后移 } else { newtail->next = l2; // 将 l2 节点挂到尾部 l2 = l2->next; // l2 后移 } newtail = newtail->next; // newtail 移动到新节点 } if (l1) newtail->next = l1; if (l2) newtail->next = l2; return newhead; }

6. 链表分割

题目描述
编写代码,以给定值 x 为基准将链表分割成两部分,所有小于 x 的结点排在大于或等于 x 的结点之前,且保持原来的相对顺序。 分割链表

思路分析
创建两个哑结点,分别作为小于 x 的部分和大于等于 x 的部分的头结点。遍历原链表,根据结点值的大小分别接到对应的链表后面。最后将两部分连接起来,注意要将大于等于部分链表的尾结点的 next 置空,避免形成环。

代码实现

typedef struct ListNode ListNode; struct ListNode* partition(struct ListNode* head, int x) { if(head==NULL) { return NULL; } ListNode* lesshead,* lesstail; ListNode* greathead,* greattail; lesshead=lesstail=(ListNode*)malloc(sizeof(ListNode)); greathead=greattail=(ListNode*)malloc(sizeof(ListNode)); ListNode* pcur=head; while(pcur) { if((pcur->val)<x) { lesstail->next=pcur; lesstail=lesstail->next; } else { greattail->next=pcur; greattail=greattail->next; } pcur=pcur->next; } greattail->next=NULL; lesstail->next=greathead->next; ListNode* ret=lesshead->next; free(lesshead); free(greathead); return ret; }

7. 链表的回文结构

题目描述
判断一个链表是否为回文结构。🔗 力扣 234. 回文链表

思路分析
三步走:1)使用快慢指针找到链表的中间结点;2)反转后半部分链表;3)比较前半部分和反转后的后半部分是否相同。最后最好将链表恢复原状(可选)。

代码实现

typedef struct ListNode Listnode; //找到中间节点 Listnode* get_middlenode(Listnode* head) { Listnode* fast=head,* slow=head; while(fast&&fast->next) { fast=fast->next->next; slow=slow->next; } return slow; } //翻转 Listnode* reverse(Listnode* middlenode) { Listnode* prev = NULL; Listnode* curr = middlenode; while (curr) { Listnode* next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; } bool isPalindrome(struct ListNode* head) { if(head==NULL) return NULL; Listnode* reversehead=reverse(get_middlenode(head)); Listnode* p1=head,* p2=reversehead; while(p1&&p2) { if(p1->val!=p2->val) { return false; } p1=p1->next; p2=p2->next; } return true; }

8. 相交链表

题目描述
输入两个链表,找出它们的第一个公共结点。🔗 力扣 160. 相交链表 

思路分析
双指针法:创建两个指针 pA 和 pB,分别指向两个链表的头结点。两个指针同时移动,当 pA 到达链表末尾时,重定向到链表B的头结点;当 pB 到达链表末尾时,重定向到链表A的头结点。最终两个指针会在相交结点相遇(若无相交,则同时指向 NULL)。

代码实现

typedef struct ListNode Listnode; struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { Listnode* pcur1=headA,* pcur2=headB; int count1=0,count2=0; while(pcur1->next) { pcur1=pcur1->next; count1++; } while(pcur2->next) { pcur2=pcur2->next; count2++; } if(pcur1!=pcur2) { return NULL; } int gap=abs(count1-count2); Listnode* longlist=headA,* shortlist=headB; if(count2>count1) { longlist=headB; shortlist=headA; } while(gap--) { longlist=longlist->next; } while(longlist!=shortlist) { longlist=longlist->next; shortlist=shortlist->next; } return longlist; }

9. 判断链表中是否有环

题目描述
给定一个链表,判断链表中是否有环。🔗 力扣 141. 环形链表

思路分析
快慢指针法:定义 slow 和 fast 指针,slow 每次走一步,fast 每次走两步。如果链表有环,则两个指针最终会在环中相遇;如果 fast 到达链表末尾(NULL),则说明链表无环。

代码实现

typedef struct ListNode listnode; bool hasCycle(struct ListNode *head) { listnode* fast=head,* slow=head; while(fast&&fast->next) { fast=fast->next->next; slow=slow->next; if(fast==slow) { return true; } } return false; }

10. 返回链表开始入环的第一个结点

题目描述
给定一个链表,返回链表开始入环的第一个结点。如果链表无环,则返回 NULL🔗 力扣 142. 环形链表 II

思路分析
在上一题判断有环的基础上,记录快慢指针相遇点。然后让一个指针从链表头开始,另一个指针从相遇点开始,两个指针每次都走一步,它们相遇的位置就是环的入口结点。数学推导可参考文档中的证明:设头结点到环入口距离为 L,环入口到相遇点距离为 X,环长度为 R,则有 L = nR - X(n为快指针在环中绕的圈数)。

代码实现

typedef struct ListNode Listnode; struct ListNode *detectCycle(struct ListNode *head) { Listnode* fast=head,* slow=head; Listnode* meet=NULL; while(fast&&fast->next) { fast=fast->next->next; slow=slow->next; if(fast==slow) { meet=fast; fast=NULL; } } if(meet==NULL) return NULL; Listnode* pcur=head; while(pcur!=meet) { meet=meet->next; pcur=pcur->next; } return meet; }

结语

链表作为数据结构的基础,其核心在于指针的灵活操作边界条件的处理。通过以上10道经典题目的练习,相信大家已经掌握了链表操作的基本技巧:

  • 哑结点的应用简化了头结点特殊情况的处理
  • 快慢指针是解决环问题、中间结点问题的利器
  • 双指针法在合并、相交等问题中表现出色
  • 反转链表的迭代和递归两种思路需要熟练掌握

链表的题目看似变化多端,但万变不离其宗。只要理解链表的结构特点,勤加练习,就能在面试和竞赛中游刃有余。建议读者在LeetCode上将这些题目逐个AC(Accept),并在解题过程中思考多种解法的优劣,逐步提升自己的算法思维。

最后,数据结构的学习是一个循序渐进的过程,链表之后还有栈、队列、树等更复杂的数据结构等待我们去探索。打好基础,方能行稳致远。

Read more

用 Python 批量下载全量 A 股历史行情数据:基于 AKShare 的高效实践

关键词:AKShare, A股数据, 股票历史行情, 量化分析, Python 金融, 断点续传 适用读者:量化交易初学者、金融数据分析师、Python 爱好者、学术研究者 💡 为什么需要本地化 A 股历史数据? 在量化投资、策略回测、因子挖掘等场景中,高质量、完整、本地存储的历史行情数据是不可或缺的基础。然而: * 商业数据接口(如 Wind、Tushare Pro)往往收费或有调用限制; * 免费接口(如早期 Tushare)可能不稳定或字段不全; * 网页爬虫易被反爬,维护成本高。 幸运的是,开源项目 AKShare 提供了免费、稳定、覆盖全面的中国金融市场数据接口,包括: * A 股日线、分钟线 * 指数、基金、期货、期权

By Ne0inhk
新手向:C语言、Java、Python 的选择与未来指南

新手向:C语言、Java、Python 的选择与未来指南

语言即工具,选对方向比埋头苦学更重要 你好,编程世界的新朋友!当你第一次踏入代码的宇宙,面对形形色色的编程语言,是否感到眼花缭乱?今天我们就来聊聊最主流的三种编程语言——C语言、Java 和 Python——它们各自是谁,适合做什么,以及未来十年谁能带你走得更远。 一、编程世界的三把钥匙:角色定位 如果把编程比作建造房屋,那么: * C语言是钢筋骨架:诞生于1972年,它直接与计算机硬件“对话”,负责构建最基础的支撑结构。 * Java是精装套房:1995年问世,以“一次编写,到处运行”闻名,擅长打造稳定、可复用的功能模块。 * Python是智能管家:1991年出生却在近十年大放异彩,像一位高效助手,用最少的指令完成复杂任务13。 二、核心差异对比:从底层到应用 1. 语言类型与设计哲学 * C语言:属于面向过程的编译型语言。代码在执行前需全部翻译成机器指令,运行效率极高,但需要开发者手动管理内存(类似自己打扫房间)15。 * Java:

By Ne0inhk
轻松上手Python:从安装到第一个Hello World程序

轻松上手Python:从安装到第一个Hello World程序

一、前言 在数字化浪潮中,编程已成为一项“新通用技能”,而Python因其近乎零门槛的入门体验,成为无数人打开代码世界的第一把钥匙。无论你是想自动化办公、分析数据,还是探索人工智能,只需一行 print("Hello World") ,就能见证计算机对你的首次回应。 本文将以零基础视角,带你完成Python环境安装、编写首个程序,并揭秘代码背后的逻辑——无需复杂配置,一台普通电脑,15分钟足矣。从这里开始,代码不再是高墙,而是你与未来对话的桥梁。 ‍ ‍ 二、下载安装 2.1 下载 打开浏览器,访问Python官方网站:https://www.python.org/,点击上方 downloads 根据你的操作系统(Windows、Mac或Linux)选择适合的版本。对于Windows用户,如果你的系统是Windows 10及以上,可以直接下载最新版本的Python(如Python 3.

By Ne0inhk
2025年中秋月亮只有94.91%圆?Python告诉你真相

2025年中秋月亮只有94.91%圆?Python告诉你真相

前言: 又是一年中秋节,祝大家中秋快乐!作为程序员的我们,还有谁和我一样在外奔波而不能回家,想和大家说一声辛苦啦!既然不能回家吃月饼、赏明月,那我是不是也能用代码写下属于自己的中秋记忆,为朋友们送去我们自己特殊的中秋祝福,让技术和传统节日碰撞出新的火花。 本文目录: * 一、月相计算:今晚的月亮到底有多圆 * 1. 月相可视化 * 二、月饼切分算法:公平分配的艺术 * 1. 经典切分策略 * 2. 进阶问题:不过圆心的切分 * 三、诗词生成:中秋凑诗 * 四、月球数据可视化:用数据看月亮 * 1. 先画月球表面:模拟环形山地形 * 2. 再做月相动画:看一个月月亮怎么变 * 五、中秋快乐,记得吃月饼🥮 * 写在最后 一、月相计算:今晚的月亮到底有多圆 今天是中秋节,刷朋友圈的时候突然想到一个问题:今年中秋的月亮到底有多圆?作为Python开发者,我决定用代码来算一算。顺便整理了几个和中秋相关的有趣项目,

By Ne0inhk