【数据结构】单链表----经典算法(一)

🌟作者介绍:友友们好我是钓鱼的猫猫,可以叫我小猫💕

⏳作者主页:钓鱼的猫猫-CSDN博客🎉

👀项目专栏:经典算法_钓鱼的猫猫的博客-CSDN博客

单链表算法需要提前知识

        🎈 1. 从零拿捏链表(一)—— 无头单向不循环链表(单链表)-CSDN博客

        🎈 2. 深入指针 ~之美学~全内容详解-CSDN博客

🎉小猫和友友们一样喜欢代码🤭。很荣幸能向大家分享我的所学所感,和大家一起进步,成为合格的卷王。✨如果文章有错误,欢迎在评论区✏️指正。那么开始今天的学习吧!😘

目录

一.移除链表元素 (双指针)

1.1 题目描述

1.2 方法思路

1.3 代码实现

二.反转链表(三指针)

2.1 题目描述

2.2 方法思路

2.3 代码实现

三.合并两个有序链表(双指针)

3.1 题目描述

3.2 解题思路

3.3 代码实现

四.链表的中间结点(快慢指针)

4.1 题目描述

4.2 解题思路

4.3 代码实现

五.约瑟夫问题(双指针)

5.1 题目描述

5.2 解题思路

5.3 代码实现

六.分割链表

6.1 题目描述

6.2 解题思路

6.3 代码实现

七.最后😘


一.移除链表元素 (双指针)

题目连接:

203. 移除链表元素 - 力扣(LeetCode)

1.1 题目描述

1.2 方法思路

        1. 检查空链表:如果头节点head为空,直接返回head。

        2. 创建虚拟头节点:创建一个新的链表,Newhead作为虚拟头节点,其值为-1,next指向NULL。Newtail用于跟踪新链表的尾部。

        3. 处理单节点链表:如果原链表只有一个节点,检查该节点的值是否等于val。如果等于,返回Newhead->next(即NULL);否则,返回原链表head。

        4. 遍历原链表:对于每个节点,如果节点的值等于val,则跳过该节点;否则,创建一个新节点,复制原节点的值,并将其插入到新链表的尾部。

        5. 返回新链表:最后返回Newhead->next,即新链表的头节点。

1.3 代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) 
{
    if(head == NULL)
        return head;
    
    ListNode* pcur = head;

    ListNode* Newhead = (ListNode*)malloc(sizeof(ListNode));
    Newhead->next = NULL;
    Newhead->val = -1;

    ListNode* Newtail = Newhead;

    if(pcur->next == NULL)
    {
        if(pcur->val == val)
        {
            return Newhead->next;
        }
        else
        {
            //尾插
            return head;
        }
    }

    while(pcur != NULL)
    {
        if(pcur->val == val)
        {
            pcur = pcur->next;
        }
        else
        {
            //尾插
            ListNode* NewNode = (ListNode*)malloc(sizeof(ListNode));
            NewNode->next = NULL;
            NewNode->val = pcur->val;

            Newtail->next = NewNode;
            pcur = pcur->next;
            Newtail = NewNode;
        }
    }
    
    return Newhead->next;
}

二.反转链表(三指针)

题目链接:

206. 反转链表 - 力扣(LeetCode)

2.1 题目描述

2.2 方法思路

      1. 如果链表为空或只有一个节点,直接返回头节点。

      2. 初始化三个指针: n1指向NULL,n2指向头节点head,n3指向头节点的下一个节点。

      3. 遍历链表,每次迭代中:

                         - 将n2的next指向n1(反转指针)。

                         - 将n1移动到n2的位置。

                         - 将n2移动到n3的位置。

                         - 如果n3不为NULL,则将n3移动到下一个节点。

     4. 当n2为NULL时,遍历结束,此时n1指向原链表的最后一个节点,即反转后的头节点。

2.3 代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */

typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) 
{
    if(head == NULL)
        return head;
    if(head->next == NULL)
        return head;

    ListNode* n1 = NULL;
    ListNode* n2 = head;
    ListNode* n3 = head->next;

    while(n2 != NULL)
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if(n3!= NULL)
        n3 = n3->next;
    }

    return n1;

}

三.合并两个有序链表(双指针)

题目链接:

21. 合并两个有序链表 - 力扣(LeetCode)

3.1 题目描述

3.2 解题思路

1. 特殊情况处理

  • 首先检查两个链表是否为空:

    • 如果 list1 为空,直接返回 list2,因为无需合并。

    • 如果 list2 为空,直接返回 list1,同样无需合并。

  • 这确保了当其中一个链表为空时,能快速返回另一个链表,提高效率。

2. 创建虚拟头节点

  • 创建一个虚拟头节点 newhead,其值为 -1next 指针初始化为 NULL。虚拟头节点的作用是为了简化链表操作,避免处理头节点的特殊情况(如头节点可能被改变)。

  • 使用一个尾指针 newtail 来跟踪新链表的当前尾部,初始时指向 newhead。这样可以在O(1)时间内完成尾部插入。

3. 比较并合并节点

  • 使用两个指针 l1 和 l2 分别遍历 list1 和 list2

  • 进入循环,比较 l1 和 l2 当前节点的值:

    • 如果 l1->val >= l2->val,则将 l2 的节点连接到新链表的尾部(newtail->next = l2),并移动 l2 和 newtail 到下一个节点。

    • 如果 l1->val < l2->val,则将 l1 的节点连接到新链表的尾部(newtail->next = l1),并移动 l1 和 newtail 到下一个节点。

  • 这个过程持续直到其中一个链表被完全遍历(即 l1 或 l2 变为 NULL)。

4. 处理剩余节点

  • 当其中一个链表遍历完后,另一个链表可能还有剩余节点(因为链表长度可能不同)。

  • 检查哪个链表还有剩余节点:

    • 如果 l1 为 NULL,说明 list1 已遍历完,将 list2 的剩余部分(l2)直接连接到新链表的尾部。

    • 如果 l2 为 NULL,说明 list2 已遍历完,将 list1 的剩余部分(l1)直接连接到新链表的尾部。

  • 这确保了所有节点都被包含在新链表中,且保持升序。

5. 返回结果

  • 返回 newhead->next,因为 newhead 是虚拟节点,其 next 指针指向合并后链表的真正头节点。

3.3 代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    if(list1 == NULL)
    {
        return list2;
    }else if(list2 == NULL)
    {
        return list1;
    }
    ListNode* newhead = (ListNode*)malloc(sizeof(ListNode));
    newhead->next = NULL;
    newhead->val = -1;   

    ListNode * newtail = newhead;

    ListNode* l1 = list1;
    ListNode* l2 = list2;

    while(l1 && l2)
    {
        if(l1->val >= l2->val)
        {
            newtail->next = l2;
            newtail = newtail->next;

            l2 = l2->next;
        }else if(l1->val < l2->val)
        {
            newtail->next = l1;
            newtail = newtail->next;

            l1 = l1->next;
        }
    }

    if(l1 == NULL)
    {
        while(l2 != NULL)
        {
            newtail->next = l2;
            newtail = newtail->next;
            l2 = l2->next;
        }

    }else if(l2 == NULL)
    {
        while(l1 != NULL)
        {
            newtail->next = l1;
            newtail = newtail->next;
            l1 = l1->next;
        }
    }
    return newhead->next;
}

四.链表的中间结点(快慢指针)

题目链接:

876. 链表的中间结点 - 力扣(LeetCode)

4.1 题目描述

4.2 解题思路

使用了快慢指针法来寻找链表的中间节点。以下是详细的解题思路分析:

1. 快慢指针法

  • 定义两个指针 fast 和 slow,初始时都指向链表的头节点 head

  • fast 指针每次移动两步,slow 指针每次移动一步。

  • 当 fast 指针到达链表末尾时,slow 指针恰好指向链表的中间节点。

2. 特殊情况处理

  • 如果链表只有一个节点(即 fast->next == NULL),直接返回头节点 head,因为中间节点就是头节点本身。

3. 循环遍历

  • 使用 while 循环,条件为 fast->next != NULL,确保 fast 指针可以移动。

  • 在循环体内:

    • 首先移动 slow 指针到下一个节点(slow = slow->next)。

    • 然后检查 fast 指针是否可以移动两步(即检查 fastfast->next 和 fast->next->next 是否存在)。

      • 如果条件满足,则移动 fast 指针两步(fast = fast->next->next)。

      • 如果条件不满足(即 fast->next->next 不存在),说明 fast 指针无法再移动两步,此时直接返回 slow 指针,因为 slow 已经指向中间节点。

4. 返回结果

  • 循环结束后,返回 slow 指针,即链表的中间节点。

为什么能返回第二个中间节点?

  • 当链表长度为偶数时,有两个中间节点。快指针每次移动两步,慢指针每次移动一步,当快指针无法再移动两步时,慢指针恰好指向第二个中间节点。例如,链表 [1,2,3,4,5,6],慢指针会指向节点4(第二个中间节点)。

4.3 代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) 
{

    ListNode* fast = head;
    ListNode* slow = head;
    if(fast->next == NULL)
     return head;

    while(fast->next != NULL)
    {
        slow = slow->next;
        if(fast && fast->next && fast->next->next)
            fast = fast->next->next;
        else 
        return slow;

    }

    return slow;
}

五.约瑟夫问题(双指针)

题目链接:

环形链表的约瑟夫问题_牛客题霸_牛客网

5.1 题目描述

5.2 解题思路

通过模拟约瑟夫问题的过程来求解,具体思路如下:

1. 特殊情况处理

  • 如果只有一个人(n == 1),那么直接返回编号1,因为不需要进行任何淘汰过程。

2. 构建环形链表

  • 创建一个单向环形链表,其中每个节点代表一个人,节点的值从1到n依次赋值。

  • 使用head指针指向链表的头节点(编号1),tail指针指向链表的尾节点(编号n),并通过tail->next = head将链表首尾相连,形成环。

3. 初始化指针和计数器

  • 使用两个指针prevpcur来遍历链表:

    • pcur指向当前报数的人。

    • prev指向pcur的前一个节点,以便于删除节点。

  • 初始化报数计数器sum为1(从1开始报数)。

4. 模拟报数和淘汰过程

  • 循环条件:当链表中不止一个节点时(即pcur->next != pcur),继续循环。

  • 内部循环:移动prevpcur指针,同时递增sum,直到sum等于m。这意味着当前pcur指向的人报数到了m,需要被淘汰。

  • 淘汰节点:将prev->next指向pcur->next,从而跳过pcur节点(即删除该节点),然后释放pcur节点(代码中未显式释放,但实际应避免内存泄漏)。之后,将pcur移动到下一个节点,并重置sum为1,从下一个人重新开始报数。

5. 返回结果

  • 当循环结束时,pcur指向链表中最后一个剩余的节点,返回其值即为最后留下的人的编号。

5.3 代码实现

/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 * 
 * @param n int整型 
 * @param m int整型 
 * @return int整型
 */
#include <stdlib.h>
typedef struct ListNode ListNode;

//创建节点
ListNode* ListNode_Apply_Node(int x)
{
    ListNode* newnode = (ListNode *)malloc(sizeof(ListNode));
    newnode->next = NULL;
    newnode->val = x;
    return newnode;
}

int ysf(int n, int m ) 
{
    if(n == 1)
        return 1;

    ListNode* head = (ListNode*)malloc(sizeof(ListNode));
    head->val = 1;
    head->next = NULL;
    ListNode* tail = head;

    //创建环形链表
    for(int i = 2;i<=n;i++)
    {
        ListNode* newnode = ListNode_Apply_Node(i);
        tail->next = newnode;
        tail = tail->next;
    }
    tail->next = head;

    ListNode* prev = tail;
    ListNode* pcur = head;

    int sum = 1;
    //开始游戏
    while(pcur->next != pcur)
    {
        while(sum != m)
        {
            prev = prev->next;
            pcur = pcur->next;
            sum++;
        }
        prev->next = pcur->next;
        pcur = pcur->next;
        sum = 1;
    }

    return pcur->val;
}

六.分割链表

题目链接:

面试题 02.04. 分割链表 - 力扣(LeetCode)

6.1 题目描述

6.2 解题思路

        尝试解决分割链表的问题,即将所有小于给定值 x 的节点移动到所有大于或等于 x 的节点之前,且不要求保持原始相对顺序。以下是代码的解题思路分析:

1. 基本思路

  • 代码旨在通过遍历原链表,将节点根据其值与 x 的比较结果重新排列:小于 x 的节点采用头插法插入新链表的头部,大于或等于 x 的节点采用尾插法插入新链表的尾部。

  • 这样,所有小于 x 的节点都会聚集在链表头部,而大于等于 x 的节点则集中在尾部。由于头插法,小于 x 的节点的顺序会被反转,但题目不要求保持原始相对顺序,因此这是可接受的。

2. 具体步骤

  • 初始化:处理空链表的情况,直接返回 NULL

  • 创建新链表:代码首先创建了一个新节点 phead,其值设置为原链表头节点的值。这实际上复制了原头节点,而不是直接使用原节点。然后设置尾指针 ptail 指向 phead,用于尾插操作。

  • 遍历原链表:从原链表的第二个节点开始遍历(因为头节点已处理)。使用 prev 和 pcur 指针来跟踪当前节点和前一个节点。

  • 节点处理

    • 如果当前节点值大于等于 x,则进行尾插:将当前节点链接到新链表的尾部,并更新 ptail

    • 如果当前节点值小于 x,则进行头插:将当前节点插入到新链表的头部,并更新 phead 指向新的头节点。

  • 结束处理:遍历结束后,将尾节点的 next 设置为 NULL,以防止链表成环,然后返回新链表的头节点 phead

6.3 代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;

struct ListNode* partition(struct ListNode* head, int x) 
{
    if(head == NULL)
    return NULL;
    ListNode* phead = (ListNode*)malloc(sizeof(ListNode));    
    phead->val = head->val;
    phead->next = NULL;


    ListNode* prev = head;
    ListNode* pcur = head->next;
    ListNode* ptail = phead;

    while(pcur != NULL)
    {
        if(pcur->val >= x)
        {
            //尾插
            ptail->next = pcur;
            ptail = ptail->next;

            prev = pcur;
            pcur = pcur->next;

        }else if(pcur->val < x)
        {
            //头插
            prev = pcur;
            pcur = pcur->next;
            prev->next = phead;
            phead = prev;
        }
    }
    ptail->next = NULL;
    return phead;   
}

七.最后😘

小猫尽力了,请见谅!😘

那么今天的学习就到这里了。友友们觉得不错的可以给个关注点赞或者收藏哦!😘感谢各位友友们的支持。希望各位大佬们自行检验哦,毕竟亲手操作让记忆更加深刻。

你的❤️点赞是我创作的动力的源泉
你的✨收藏是我奋斗的方向
你的🙌关注是对我最大的支持
你的✏️评论是我前进的明灯
创作不易,希望友友们支持一下小猫吧😘

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值