链表经典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

2020年信奥赛C++提高组csp-s初赛真题及答案解析(选择题11-15)

2020年信奥赛C++提高组csp-s初赛真题及答案解析(选择题11-15)

2020年信奥赛C++提高组csp-s初赛真题及答案解析(选择题11-15) 第 11 题:小明想通过走楼梯来锻炼身体,假设从第 1 层走到第 2 层消耗 10 卡热量,接着从第 2 层走到第 3 层消耗 20 卡热量,再从第 3 层走到第 4 层消耗 30 卡热量,依此类推,从第 k 层走到第 k+1 层消耗 10k卡热量 (k>1)。如果小明想从 1 层开始,通过连续向上爬楼梯消耗 1000 卡热量,至少要爬到第几层楼? ( )。 A. 14 B. 16

By Ne0inhk
C++之基于正倒排索引的Boost搜索引擎项目usuallytool部分代码及详解

C++之基于正倒排索引的Boost搜索引擎项目usuallytool部分代码及详解

这部分是通用工具部分的代码,简单来说就是这份代码里面的函数会在项目的其他多个部分里面被使用,所以我们专门创建一个部分用来存储这些代码。 1.FileUtil 这个类就是专门用来读取文件用的,这个代码从指定的文件路径读取文件内容,将读取到的内容(按行读取)追加到传入的字符串指针(out)所指向的字符串中;同时,该方法会返回一个布尔值,用于标识读取操作是否成功 —— 若文件成功打开并完成读取,返回 true;若文件打开失败(如路径错误等),则输出错误信息并返回 false。 文件以二进制输入模式打开,读取过程中不会修改原文件内容。 class FileUtil{ public: static bool ReadFile(const std::string &file_path,std::string *out) { //下面这行代码就是在打开文件,并通过ifstream定义一个对象in,用于关联特定的文件 std::ifstream in(file_path,std::ios::in

By Ne0inhk
墨色规则与血色节点:C++红黑树设计与实现探秘

墨色规则与血色节点:C++红黑树设计与实现探秘

前言     前几天攻克了AVL树,我们已然是平衡二叉树的强者。但旅程还未结束,下一个等待我们的,是更强大、也更传奇的**终极BOSS**——红黑树。它不仅是map和set的强大心脏,更是C++ STL皇冠上的明珠。准备好了吗?让我们一起揭开它的神秘面纱。 1.红黑树的概念 1.1.红黑树是什么     红黑树是一科二叉搜索树,他的每个节点增加一个存储为代表着该节点的颜色,和它的名字一样,节点的颜色可以是红色或者是黑色。通过对任何一条根到叶子的路径上各个节点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是接近平衡的。     红黑树是被很多条规则进行束缚的二叉搜索树,通过这些规则,可以保证红黑树没有一条路径会比其他路径长出2倍,并且保持其相对平衡,下面我来讲述一下这些规则。 1.2.红黑树的规则     1.每个节点不是黑色的就是红色的(这肯定,不然不会被叫做红黑树了)。     2.根节点必须是黑色的     3.如果一个节点是红色的,则它的两个孩子节点必须是黑色的,也就是说任意一条路径上并不会出现连续的红色的节点。     4.对于任意的一个

By Ne0inhk