C语言编程实战:云刷题 - day5

在这里插入图片描述

🌈这里是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,再将pnext指向新链表的当前头node,随后更新nodepp成为新的头)。整个过程只需一次遍历,时间复杂度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)nm为两链表长度),空间复杂度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;
}

代码逻辑解释

  1. 环形链表构建creatcircle函数创建一个包含n个节点的环形链表,每个节点的值代表一个人的位置,尾节点指向头节点形成闭环。
  2. 遍历与删除:使用prev(前驱节点)和pcur(当前节点)两个指针遍历链表,每次数到m时删除pcur节点,并调整指针指向。
  3. 终止条件:当链表中只剩一个节点时(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;
}

代码逻辑解释

  1. 初始条件pcur从第二个节点开始遍历,prev跟随其作为前驱节点,避免断链。
  2. 节点移动规则
    • pcur->val < x:将pcur节点从原位置移除,插入到链表头部(成为新的头节点)。
    • pcur->val >= x:指针正常后移,不做额外操作。
  3. 保持顺序:通过“将符合条件的节点插入头部”的方式,保证小于x的节点按原有相对顺序排列在链表前端。

  • 本节完…
评论 8
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值