数据结构之单链表OJ复盘

1.移除链表元素

https://leetcode.cn/problems/remove-linked-list-elements/description/

c 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* removeElements(struct ListNode* head, int val) {
    struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
    dummy->next = head;
    struct ListNode* prev = dummy;
    struct ListNode* curr = head;
    while (curr != NULL) {
        if (curr->val == val) {
            prev->next = curr->next;
            free(curr);         
            curr = prev->next;
        } else {
            prev = curr;
            curr = curr->next;
        }
    }
    struct ListNode* newHead = dummy->next;
    free(dummy);
    return newHead;
}

我们来理清一下思路以及画图,数据结构做题不画图玩不了一点(嘻嘻)。

解题思路:
因为头节点也可能被删除,所以我们需要一个虚拟头节点(dummy node)来统一操作。设置一个虚拟头节点 dummy,其 next 指向原链表头。然后使用两个指针 prev 和 curr 遍历链表,prev 始终指向当前节点的前一个节点,curr 指向当前节点。

  • 如果 curr.val == val,则删除该节点:prev.next = curr.next,然后 curr 后移。

  • 否则,prev 和 curr 都后移,最后返回 dummy.next 作为新的头节点。

其实思路还是很好懂的,如果不懂的话我们就来画图看一下:

这是我们的初始情况对吧,val初始值为6不为3,所以我们的prev和curr都往后移,也就是:

c 复制代码
prev = curr;
curr = curr->next;

对吧,直到,prev指向2,curr指向6,如下所示:

此时,我们需要让prev跳过6,也就是prev->next = curr->next;,再接着移动curr到下一个位置即可。

以此往复1,最后返回dummy->next即可。

2.反转链表

2.1迭代法

https://leetcode.cn/problems/reverse-linked-list/description/

c 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head)
{
    ListNode* prev = NULL;
    ListNode* curr = head;
    
    while(curr)
    {
        ListNode* next = curr->next;
        curr->next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

反转思路:

我们需要改变每个节点的 next 指针,让它指向前一个节点。

为了做到这一点,需要三个指针:

  • prev:指向当前节点的前一个节点(初始为 NULL)

  • curr:指向当前正在处理的节点(初始为 head)

  • next:临时保存当前节点的下一个节点(防止链表断开后丢失)

核心步骤(循环直到 curr 为 NULL):

  • 保存 curr->next 到 next(因为马上要修改 curr->next)。

  • 将 curr->next 指向 prev(反转指针)。

  • 移动 prev 到 curr(准备处理下一个节点)。

  • 移动 curr 到 next(继续遍历)。

循环结束后,prev 指向原链表的最后一个节点,也就是新链表的头节点。

初始条件:

接下来先保存next,然后反转指针,接下来移动指针:

接下来依次类推:

直至最后完成反转即可。

边界情况

  • 空链表:head == NULL,循环不执行,直接返回 prev(也是 NULL),正确。

  • 只有一个节点:循环一次,curr->next 指向 NULL,返回该节点本身。

2.2头插法

头插法是一种构建链表的方法:每次新节点都插入到链表的最前面,成为新的头节点。

例如:依次插入1、2、3,头插的结果是 3→2→1(因为1插入后链表为[1],再插入2成为[2→1],再插入3成为[3→2→1])。
而本题正是利用这个特性:把原链表的节点按原顺序依次头插到新链表,得到的就是反转后的链表。

c 复制代码
struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* p = head;   // p 用来遍历原链表,初始指向原头
    head = NULL;                  // 将 head 重新用作新链表的头,初始为空链表
    while (p) {                   // 当 p 不为空,即还有节点要处理
        struct ListNode* s = p;   // s 保存当前要拆下的节点(即 p 指向的节点)
        p = p->next;              // p 先移动到下一个节点(保存下一个位置,防止丢失)
        s->next = head;           // 将 s 的 next 指向当前新链表的头(即头插)
        head = s;                  // 更新新链表的头为 s
    }
    return head;                   // 返回新链表的头
}

初始:

第一次循环(处理节点1)

  • s = p:s 指向节点1。

  • p = p->next:p 移动到节点2(此时 p 指向节点2,原链表剩余部分由 p 掌握)。

  • s->next = head:此时 head 为 NULL,所以 s->next 指向 NULL,即节点1变成新链表的最后一个节点(因为它的 next 是 NULL)。

  • head = s:新链表的头更新为节点1。

如下图所示:

下一次循环时,s又指向节点2,p指向节点3,s继续头插,节点2成为新的头节点,依次类推,在这里就不再画图了

3.链表的中间节点

https://leetcode.cn/problems/middle-of-the-linked-list/description/

3.1常规易懂法

c 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head)
{
    ListNode* pcur = head;
    int n = 0;
    while(pcur)
    {
        pcur=pcur->next;
        n++;
    }
    int steps = n/2;
    pcur = head;
    while(steps--)
    {
        pcur=pcur->next;
    }
    return pcur;
}

解题思路:

首先,题目要求返回我们的中间节点对吧,那么我们可不可以去定义个指针去遍历链表,找出总数,接下来我们不是要返回中间节点吗,找到它就好了,所以我们需要再一次去遍历链表,

这里为什么是 int steps = n/2;呢?因为你会发现,无论是当n为奇数时,假设n等于5,那么steps等于2,我们从节点1开始,走两次不就是节点3嘛,当n为偶数时,假设为6,steps等于3,不就是从1走到4嘛,正好符合我们题意,所以接下来找到中间节点,直接返回就可以了。

3.2快慢指针

c 复制代码
**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) 
{
    ListNode* slow = head;
    ListNode* fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

这是个什么意思呢?

想象两个人在一条直线上跑步:

  • 一个人跑得快,每次跑两步

  • 一个人跑得慢,每次跑一步

  • 当快的人跑到终点时,慢的人刚好在中间位置。

在链表中,我们定义两个指针:

  • slow:每次走一步

  • fast:每次走两步

初始时,两个指针都指向头节点。然后同时移动:

  • slow = slow->next

  • fast = fast->next->next

  • 当fast到达末尾(即fast为NULL或者fast->next为NULL)时,slow指向的就是中间节点。

注意:因为fast一次走两步,所以要确保每次移动前检查fast和fast->next不为空。

我们来画图举个例子看一下:

初始情况:当为奇数时

第一次:

第二次:

我们会发现确实如我们所说那样,偶数情况我们不再演示,原理相同。

4.链表分割

https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70

cpp 复制代码
/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
  public:
    ListNode* partition(ListNode* pHead, int x) {
        // 创建两个哑节点(使用 new 分配)
        ListNode* smallHead = new ListNode(0);
        ListNode* largeHead = new ListNode(0);
        ListNode* smallTail = smallHead;
        ListNode* largeTail = largeHead;

        ListNode* curr = pHead;
        while(curr)
        {
            ListNode* next = curr->next;
            if(curr->val<x)
            {
                smallTail->next=curr;
                smallTail = curr;
            }
            else 
            {
                largeTail->next=curr;
                largeTail = curr;
            }
            curr->next = NULL;
            curr = next;
        }
        smallTail->next=largeHead->next;
        ListNode* newhead = smallHead->next;

        delete smallHead;
        delete largeHead;
        
        return newhead;
    }
};

解题思路:
我们使用两个辅助链表(通过哑节点实现)来分别收集小于 x 和大于等于 x 的节点,最后将它们连接起来。具体步骤:

创建两个哑节点(dummy head):

  • smallHead:用于存放小于 x 的链表头(它的 next 指向第一个小于 x 的节点)

  • largeHead:用于存放大于等于 x 的链表头

    同时创建两个尾指针 smallTail 和 largeTail,初始指向各自的哑节点。

遍历原链表:

  • 用 cur 指向当前节点,先保存 cur->next 到 next,防止断开后丢失后续节点。

  • 如果 cur->val < x,则将其接到 smallTail 后面,然后移动 smallTail 到该节点。

  • 否则接到 largeTail 后面,移动 largeTail。

  • 注意:将当前节点接入新链表后,要将其 next 置为 NULL,确保它不再指向原链表的下一个节点(因为原顺序已不再需要)。

  • 然后 cur = next 继续遍历。

连接两个链表:

  • 将 smallTail->next 指向 largeHead->next(即大于等于链表的第一个实际节点)。

  • 此时 largeTail 的 next 已经是 NULL(因为我们在接入每个节点时都置了 NULL),所以无需再处理。

返回新链表头:

  • 新链表的头是 smallHead->next。

  • 最后记得释放两个哑节点(避免内存泄漏)。

初始状态:

接下来:

往复循环即可,以此类推,最后连接链表的时候需要注意:

c 复制代码
将 smallTail->next 指向 largeHead->next

循环完是这样的:

c 复制代码
small: s -> 1 -> 2 -> 2 -> NULL
large: l -> 4 -> 3 -> 5 -> NULL

最后连接即可。

5.链表的回文结构

https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa

cpp 复制代码
/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
  public:
    // 反转链表函数(返回新头)
    ListNode* reverseList(ListNode* head) 
    {
        ListNode* prev = NULL;
        ListNode* cur = head;
        while (cur) 
        {
            ListNode* next = cur->next;
            cur->next = prev;
            prev = cur;
            cur = next;
        }
        return prev;
    }

    bool chkPalindrome(ListNode* A) 
    {
        if (A == NULL || A->next == NULL) 
        return true; // 空或只有一个节点是回文

        // 1. 找到中间节点(快慢指针)
        ListNode* slow = A;
        ListNode* fast = A;
        while (fast && fast->next) 
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        // 此时 slow 指向中间节点(如果偶数,指向第二个中间节点)

        // 2. 反转后半部分(从 slow 开始)
        ListNode* right = reverseList(slow); // 反转后的头
        ListNode* left = A;

        // 3. 比较
        bool isPalindrome = true;//假设
        while (right) 
        {
            if (left->val != right->val) 
            {
                isPalindrome = false;
                break;
            }
            left = left->next;
            right = right->next;
        }

        return isPalindrome;
    }
};

这道题的代码看似很长,但其实逻辑还是比较简单的,就是把我们的前几道题给综合了那么一小下

解题思路:(以链表 1 -> 2 -> 2 -> 1 为例)
核心思想:找到中点,反转后半部分,然后比较

  • 找到链表的中间节点(使用快慢指针)。

  • 将后半部分链表反转。

  • 比较前半部分和后半部分是否相等。

由于之前说过查找中间节点和反转链表的相关逻辑与题目,在此来说一下关于比较的逻辑:

比较前半部分和后半部分:

现在有两个链表头:

  • left = head(指向第一个节点1)

  • right = 反转后的头(指向最后一个节点1)

依次比较 left->val 和 right->val,然后同时后移,直到 right 为 NULL(因为后半部分可能比前半部分短一个,如果奇数个节点,前半部分多一个中间节点,但中间节点不用比较,因为回文时中间节点任意)。

对于偶数个节点,比较过程:

  • left=1, right=1,相等,都后移:left=2, right=2

  • left=2, right=2,相等,right 后移为 NULL,结束。

  • 全部相等,返回 true。

6.相交链表

https://leetcode.cn/problems/intersection-of-two-linked-lists/description/

c 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
    ListNode* currA = headA;
    ListNode* currB = headB;
    int lenA = 0;
    int lenB = 0;
    while(currA)
    {
        currA=currA->next;
        lenA++;
    }
    while(currB)
    {
        currB=currB->next;
        lenB++;
    }
    int gap = abs(lenA-lenB);
    ListNode* longlist = headA;
    ListNode* shortlist = headB;
    if(lenA<lenB)
    {
        longlist = headB;
        shortlist = headA;
    }
    while(gap--)
    {
        longlist=longlist->next;
    }
    while(shortlist!=longlist)
    {
        longlist=longlist->next;
        shortlist=shortlist->next;
    }
    return longlist;
}

代码逻辑思路:

计算两个链表的长度:

  • 用 currA 遍历链表 headA,统计节点个数 lenA。

  • 用 currB 遍历链表 headB,统计节点个数 lenB。

  • 遍历结束后,currA 和 currB 都指向 NULL,但长度值已记录。

确定长链表和短链表:

  • 计算长度差 gap = abs(lenA - lenB)。

  • 假设 longlist 指向较长的链表头,shortlist 指向较短的链表头。

  • 通过比较 lenA 和 lenB 来正确赋值。

让长链表先走 gap 步(差距步):

  • 这样长链表和短链表剩余的长度相同。

  • 如果链表原本相交,此时两个指针距离相交点的步数相同。

同时移动两个指针,直到相遇:

  • 在 while (shortlist != longlist) 循环中,每次两个指针各走一步。

  • 如果链表相交,它们会在相交点相遇,此时返回该节点。

  • 如果链表不相交,它们最终会同时到达 NULL,循环条件不成立,返回 NULL

今天就先到这里,下次我们继续来复盘更多单链表经典题目~

相关推荐
sali-tec1 小时前
C# 基于OpenCv的视觉工作流-章32-圆环卷收
图像处理·人工智能·opencv·算法·计算机视觉
OYangxf1 小时前
【力扣hot100】哈希专题
算法·leetcode·哈希算法
小乔的编程内容分享站1 小时前
C语言笔记之结构体第二篇
c语言·开发语言·笔记
CoovallyAIHub1 小时前
32K Star!港大开源Nanobot:4000行代码打造最轻量OpenClaw平替
深度学习·算法·计算机视觉
CoderCodingNo1 小时前
【GESP】C++六级/五级练习题 luogu-P1323 删数问题
开发语言·c++·算法
飞Link1 小时前
终结序列建模:Transformer 架构深度解析与实战指南
人工智能·python·深度学习·算法·transformer
We་ct1 小时前
LeetCode 211. 添加与搜索单词 - 数据结构设计:字典树+DFS解法详解
开发语言·前端·数据结构·算法·leetcode·typescript·深度优先
一叶落4381 小时前
LeetCode 202. 快乐数(C语言详解 | 三种解法 | 哈希表 + 快慢指针)
c语言·数据结构·算法·leetcode·散列表
吃着火锅x唱着歌1 小时前
LeetCode 1190.反转每对括号间的子串
算法·leetcode·职场和发展
再难也得平1 小时前
力扣238. 除自身以外数组的乘积(Java解法)
python·算法·leetcode