跳到主要内容链表经典 OJ 题目解析与 C 语言实现 | 极客日志C算法
链表经典 OJ 题目解析与 C 语言实现
链表十大经典 OJ 题目涵盖删除节点、反转链表、寻找中间结点、倒数第 k 个结点、合并有序链表、链表分割、回文结构判断、相交链表检测、环检测及入环节点查找。内容提供思路分析与 C 语言代码实现,涉及快慢指针、哑结点、双指针等核心技巧,帮助读者掌握链表操作基础与面试考点。
SqlMaster0 浏览 引言
链表是一种基础且重要的数据结构,在程序设计中有着广泛的应用。由于其物理存储的非连续性,链表在插入、删除操作上具有独特的优势,但也给某些操作(如随机访问)带来了挑战。在技术面试和算法竞赛中,链表相关的题目出现频率极高,熟练掌握链表的常见操作和经典问题的解法,是每个程序员必备的技能。本文精选了 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. 反转链表
思路分析
迭代法:使用三个指针 prev、curr、next。初始时 prev 为 NULL,curr 指向头结点。每次将 curr->next 指向 prev,然后三个指针整体后移,直到 curr 为 NULL,此时 prev 即为新头结点。
代码实现
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
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. 链表的中间结点
思路分析
快慢指针法:定义两个指针 slow 和 fast,初始都指向头结点。slow 每次走一步,fast 每次走两步。当 fast 到达链表末尾时,slow 恰好指向中间结点。若链表长度为偶数,fast 为空时 slow 指向第二个中间结点。
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
ListNode* slow,*fast;
slow=fast=head;
while(fast&&fast->next) {
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
4. 链表中倒数第 k 个结点
思路分析
双指针法:定义两个指针 fast 和 slow,初始都指向头结点。先让 fast 指针向前移动 k 步,然后 fast 和 slow 同时移动。当 fast 到达链表末尾时,slow 恰好指向倒数第 k 个结点。需要注意 k 大于链表长度的情况。
typedef struct ListNode ListNode;
struct ListNode* trainingPlan(struct ListNode* head, int k) {
ListNode* fast=head,* slow=head;
if(head==NULL) return NULL;
while(k--) {
fast=fast->next;
}
while(fast) {
fast=fast->next;
slow=slow->next;
}
return slow;
}
5. 合并两个有序链表
思路分析
迭代法:创建一个虚拟头结点 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;
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->next;
} else {
newtail->next = l2;
l2 = l2->next;
}
newtail = newtail->next;
}
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. 链表的回文结构
思路分析
三步走: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 true;
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. 相交链表
思路分析
双指针法:创建两个指针 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. 判断链表中是否有环
思路分析
快慢指针法:定义 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. 返回链表开始入环的第一个结点
思路分析
在上一题判断有环的基础上,记录快慢指针相遇点。然后让一个指针从链表头开始,另一个指针从相遇点开始,两个指针每次都走一步,它们相遇的位置就是环的入口结点。数学推导可参考文档中的证明:设头结点到环入口距离为 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),并在解题过程中思考多种解法的优劣,逐步提升自己的算法思维。
最后,数据结构的学习是一个循序渐进的过程,链表之后还有栈、队列、树等更复杂的数据结构等待我们去探索。打好基础,方能行稳致远。