
🌈这里是say-fall分享,感兴趣欢迎三连与评论区留言
🔥专栏:《C语言从零开始到精通》
《C语言编程实战》
《数据结构与算法》
《小游戏与项目》
💪格言:做好你自己,你才能吸引更多人,并与他们共赢,这才是你最好的成长方式。
前言:
本篇内容给大家带来一些关于链表的经典算法题
文章目录
正文:
1. 链表的中间节点
- 这道题是让我们找到单链表的中间节点,如果有两个中间节点(比如链表长度为偶数时),就返回第二个。
- 题目链接在这里:力扣:链表的中间结点
思路解析
最直接的想法可能是先遍历一遍链表算出长度,再从头走一半的距离找到中间节点。但这里有个更巧妙的方法——快慢指针。
原理很简单:让两个指针同时从链表头出发,慢指针一次走1步,快指针一次走2步。当快指针走到链表末尾时,慢指针刚好走到中间位置。
- 如果链表长度是奇数(比如5个节点),快指针走到最后一个节点时,慢指针就在第3个节点(正中间)。
- 如果链表长度是偶数(比如4个节点),快指针走到倒数第二个节点的next(也就是NULL)时,慢指针刚好在第3个节点(第二个中间节点),符合题目要求。
代码实现
// 先定义链表节点类型(实际做题时力扣会提供)
typedef struct ListNode SLTNode;
struct ListNode* middleNode(struct ListNode* head)
{
SLTNode* slow, * fast; // 定义慢指针和快指针
slow = head; // 慢指针从表头出发
fast = head; // 快指针也从表头出发
// 循环条件:快指针没走到尾,且快指针的下一个节点也不是尾
// (避免fast->next为NULL时,访问fast->next->next出错)
while (fast && fast->next)
{
slow = slow->next; // 慢指针走1步
fast = fast->next->next; // 快指针走2步
}
return slow; // 循环结束时,慢指针就在中间节点
}
举个例子:
- 对于链表
1->2->3->4->5,快指针走到5时(fast->next为NULL),慢指针在3,直接返回3。 - 对于链表
1->2->3->4,快指针走到4的next(NULL)时,慢指针在3,返回3(第二个中间节点)。
这种方法只需要遍历一次链表,时间复杂度是O(n),空间复杂度O(1),比两次遍历的方法更高效。
2. 链表的中间节点(思路二:遍历两次)
// 功能:找到链表的中间节点(若有两个中间节点,返回第二个)
// 思路:两次遍历法 - 第一次统计链表长度,第二次定位到中间位置
struct ListNode* middleNode2(struct ListNode* head)
{
int count = 0; // 用于记录链表的总节点数
SLTNode* pcur = head; // 遍历指针,初始指向头节点
// 第一次遍历:统计链表长度
while (pcur) // 当指针不为空时,继续遍历
{
pcur = pcur->next; // 指针后移
count++; // 每移动一次,节点数+1
}
// 第二次遍历:定位到中间节点
pcur = head; // 重置指针到头部
// 移动次数为总长度的一半(整数除法,如长度5移动2次到第3个节点,长度4移动2次到第3个节点)
for (int i = 0; i < count / 2; i++)
{
pcur = pcur->next; // 指针后移,直到中间位置
}
return pcur; // 返回中间节点
}
解释:
该方法通过两次遍历实现功能:第一次遍历统计链表总节点数count,第二次根据count/2计算中间位置,将指针移动到对应节点。时间复杂度为O(n)(两次遍历总长度),空间复杂度O(1)(仅用常数个变量),逻辑直观,适合对时间效率要求不极致的场景。
3. 反转链表(思路二:新链表头插法)
// 功能:反转单链表,返回反转后的头节点
// 思路:新链表头插法 - 遍历原链表,将每个节点依次插入新链表的头部
struct ListNode* reverseList2(struct ListNode* head)
{
SLTNode* p = head; // 遍历原链表的指针,初始指向头节点
SLTNode* node = NULL; // 记录反转后新链表的头节点(初始为空)
while (p) // 遍历原链表,直到所有节点处理完毕
{
SLTNode* next = p->next; // 保存当前节点的下一个节点(防止断链)
p->next = node; // 将当前节点的next指向新链表的头(插入头部)
node = p; // 更新新链表的头节点为当前节点
p = next; // 移动到原链表的下一个节点
}
return node; // 最终node即为反转后的头节点
}
解释:
核心逻辑是通过“头插法”构建新链表:遍历原链表时,每个节点p先保存其下一个节点next,再将p的next指向新链表的当前头node,随后更新node为p(p成为新的头)。整个过程只需一次遍历,时间复杂度O(n),空间复杂度O(1)(无额外空间开销),是反转链表的高效解法。
4. 合并两个有序链表
// 功能:合并两个升序链表,返回一个新的升序链表
// 思路:哨兵节点+尾插法 - 比较两链表节点值,依次将较小节点插入新链表
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
// 边界处理:若其中一个链表为空,直接返回另一个链表
if (list1 == NULL)
{
return list2;
}
if (list2 == NULL)
{
return list1;
}
SLTNode* phead = NULL; // 新链表的头节点
SLTNode* pcur = NULL; // 新链表的尾指针(用于尾插)
SLTNode* pcur1 = list1; // 遍历list1的指针
SLTNode* pcur2 = list2; // 遍历list2的指针
// 创建哨兵节点(简化头节点判断,避免空链表处理逻辑)
phead = pcur = (SLTNode*)malloc(sizeof(SLTNode));
assert(phead && pcur); // 确保内存分配成功
// 遍历两个链表,比较节点值并尾插较小节点
while (pcur1 && pcur2) // 当两个链表都未遍历完时
{
if (pcur1->val < pcur2->val) // 若list1当前节点值更小
{
pcur->next = pcur1; // 尾插list1的节点
pcur = pcur->next; // 尾指针后移
pcur1 = pcur1->next; // list1指针后移
}
else // 若list2当前节点值更小或相等
{
pcur->next = pcur2; // 尾插list2的节点
pcur = pcur->next; // 尾指针后移
pcur2 = pcur2->next; // list2指针后移
}
}
// 处理剩余节点(其中一个链表已遍历完,直接链接另一个链表的剩余部分)
if (pcur2)
{
pcur->next = pcur2;
}
else
{
pcur->next = pcur1;
}
// 释放哨兵节点,更新新链表头节点
SLTNode* prev = phead; // 暂存哨兵节点地址
phead = phead->next; // 哨兵节点的next即为真实头节点
free(prev); // 释放哨兵节点,避免内存泄漏
prev = NULL;
return phead; // 返回合并后的链表头节点
}
解释:
该方法通过“哨兵节点”简化新链表的头节点处理,无需单独判断空链表插入逻辑。核心步骤是:同时遍历两个链表,每次将值较小的节点尾插到新链表,直到其中一个链表遍历完毕,再将剩余节点直接链接到新链表尾部。时间复杂度O(n+m)(n和m为两链表长度),空间复杂度O(1)(仅用常数个指针,哨兵节点内存最终释放)。
5. 移除链表元素(思路:新链表尾插法)
// 功能:移除链表中所有值为val的节点,返回新链表
// 思路:哨兵节点+尾插法 - 遍历原链表,将非val节点尾插到新链表
struct ListNode* removeElements1(struct ListNode* head, int val) {
// 创建哨兵节点(简化新链表头节点的判断,统一插入逻辑)
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
dummy->next = NULL; // 哨兵节点初始无后继
struct ListNode* tail = dummy; // 新链表的尾指针(始终指向最后一个节点,用于尾插)
struct ListNode* p = head; // 遍历原链表的指针
// 遍历原链表所有节点
while (p != NULL) {
if (p->val != val) { // 若当前节点值不等于val,保留该节点
tail->next = p; // 将节点尾插到新链表
tail = p; // 更新尾指针到新插入的节点
}
p = p->next; // 无论是否保留,都移动到下一个节点
}
// 关键:新链表末尾必须置空(防止原链表尾部有val节点时,残留无效链接)
tail->next = NULL;
// 释放哨兵节点,获取新链表的真实头节点
struct ListNode* newHead = dummy->next; // 哨兵节点的next即为新头
free(dummy); // 释放哨兵节点,避免内存泄漏
dummy = NULL;
return newHead; // 返回处理后的新链表
}
解释:
通过“哨兵节点”统一新链表的插入逻辑,无需单独处理“新链表为空时插入第一个节点”的情况。遍历原链表时,仅将值不等于val的节点尾插到新链表,最后手动将新链表尾节点的next置空(避免残留原链表的无效节点)。时间复杂度O(n),空间复杂度O(1)(哨兵节点内存最终释放),能处理所有边界情况(如链表为空、全为val节点等)。
6. 环形链表的约瑟夫问题
这段代码使用环形链表来模拟约瑟夫环问题,通过不断遍历和删除节点来找到最后存活的位置。
// 定义链表节点类型别名
typedef struct ListNode SLTNode;
// 创建新节点的函数
SLTNode* Buynode(int x)
{
// 为新节点分配内存
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
// 检查内存分配是否成功,失败则退出程序
if(node == NULL)
{
exit(1);
}
// 初始化节点:指针置空,值设为x
node->next = NULL;
node->val = x;
return node;
}
// 创建包含n个节点的环形链表
SLTNode* creatcircle(int n)
{
// 创建第一个节点(值为1)
SLTNode* phead = Buynode(1);
SLTNode* ptail = phead;
// 循环创建剩余n-1个节点(值从2到n)
for (int i = 2; i <= n; i++)
{
ptail->next = Buynode(i); // 链接新节点
ptail = ptail->next; // 尾指针后移
}
// 将链表首尾相连,形成环形结构
ptail->next = phead;
return ptail; // 返回尾节点(便于后续删除操作)
}
// 解决约瑟夫环问题:n个人围成圈,每次数到m的人出列,返回最后存活者的位置
int ysf(int n, int m)
{
// 创建环形链表,prev指向尾节点
SLTNode* prev = creatcircle(n);
// pcur指向头节点(prev的下一个节点)
SLTNode* pcur = prev->next;
// 计数变量,用于数到m
int count = 1;
// 循环条件:链表中不止一个节点(当只剩一个节点时,pcur->next == pcur)
while (pcur->next != pcur)
{
// 如果数到m,删除当前节点
if (count == m)
{
prev->next = pcur->next; // 跳过当前节点,链接前后节点
free(pcur); // 释放被删除节点的内存
pcur = prev->next; // pcur指向新的当前节点
count = 1; // 重置计数器
}
else
{
// 未数到m,继续后移指针
prev = pcur; // prev跟随pcur后移
pcur = pcur->next; // pcur后移
count++; // 计数器加1
}
}
// 返回最后存活节点的值(即位置)
return pcur->val;
}
代码逻辑解释:
- 环形链表构建:
creatcircle函数创建一个包含n个节点的环形链表,每个节点的值代表一个人的位置,尾节点指向头节点形成闭环。 - 遍历与删除:使用
prev(前驱节点)和pcur(当前节点)两个指针遍历链表,每次数到m时删除pcur节点,并调整指针指向。 - 终止条件:当链表中只剩一个节点时(
pcur->next == pcur),循环结束,返回该节点的值。
7. 分割链表(思路一:头插小元素)
这段代码实现了将链表按给定值x分割的功能:所有小于x的节点移到链表前面,大于等于x的节点留在后面,且保持原有相对顺序。
// 分割链表:将小于x的节点移到前面,大于等于x的节点留在后面
struct ListNode* partition(struct ListNode* head, int x)
{
// 处理空链表的情况
if (head == NULL)
{
return NULL;
}
// pcur从第二个节点开始遍历
struct ListNode* pcur = head->next;
// prev指向pcur的前驱节点,初始为头节点
struct ListNode* prev = head;
// 遍历链表
while (pcur)
{
// 如果当前节点值小于x,需要将其移到链表头部
if (pcur->val < x)
{
// 保存pcur的下一个节点(防止断链)
struct ListNode* next = pcur->next;
// 将当前节点从原位置移除
prev->next = next;
// 将当前节点插入到头节点之前(成为新的头节点)
pcur->next = head;
head = pcur;
// pcur指向原保存的下一个节点,继续遍历
pcur = next;
}
else
{
// 当前节点值大于等于x,无需移动,继续后移指针
prev = pcur;
pcur = pcur->next;
}
}
// 返回分割后的链表头节点
return head;
}
代码逻辑解释:
- 初始条件:
pcur从第二个节点开始遍历,prev跟随其作为前驱节点,避免断链。 - 节点移动规则:
- 若
pcur->val < x:将pcur节点从原位置移除,插入到链表头部(成为新的头节点)。 - 若
pcur->val >= x:指针正常后移,不做额外操作。
- 若
- 保持顺序:通过“将符合条件的节点插入头部”的方式,保证小于x的节点按原有相对顺序排列在链表前端。
- 本节完…
1037





