zl程序教程

您现在的位置是:首页 >  IT要闻

当前栏目

【牛客网面试必刷TOP101】链表篇(一)

2023-04-18 16:14:23 时间

一、前言

链表是数据结构中重要的一个章节,他的重要性也不言而喻,在未来不管是笔试还是面试都会遇到这类的题目,所以接下来我就会把一些链表的常考的题目全部整理出来供大家学习指正。


二、学习刷题网站

点击下面链接即可进行刷题学习
开始刷题

1.推荐的原因

刷题网站何其多,但好的刷题网站却不多,以下几点就是我推荐的原因:
1️⃣全面

里面有很多资料,不管是刷题还是学习还是面经等等

2️⃣大众

首先用的人很多,可以看到很多的题解,其次如果有问题也会有很多人回答

3️⃣熟悉oj环境

我们以后找工作的时候很多公司都会用这个网站,我们可以提前熟悉环境


三、刷题

先说明一下一些题目取自牛客网面试必刷TOP101
里面的一些题目在我以前的文章详细写到过,如果没有用新的方法就不会再做讲解
链表题目(一)
链表题目(二)
环状链表

<1>反转链表

题目链接
描述:

给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。
数据范围:0 ≤ n ≤ 1000
要求:空间复杂度 O(1) ,时间复杂度 O(n) 。
如当输入链表{1,2,3}时,
经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。
以上转换过程如下图所示:

在这里插入图片描述
示例1:

输入:{1,2,3}
返回值:{3,2,1}

示例2:

输入:{}
返回值:{}
说明:空链表则输出空

思路分析:

以前的文章里面讲过两个方法:
1.暴力改方向,三指针直接改
2.头插法
这里讲一下新方法:

递归法

递归主要是要把大事化小。用第一个元素来说,假设后面已经逆序,把后面的所有元素当成一个整体,把第一个元素移到最后就完成了逆序。然后继续第二个第三个元素递归下去,最终完成逆序。

而递归最重要的是结束条件,当递归到最后一个元素就已经全部完成了逆序。

struct ListNode* ReverseList(struct ListNode* pHead ) {
    // write code here
    if(pHead == NULL || pHead->next == NULL)
    {
        return pHead;
    }
    struct ListNode* tmp = ReverseList(pHead->next);
    pHead->next->next = pHead;
    pHead->next = NULL;
    return tmp;
}

这里注意pHead->next每次都是指向tmp链表的最后一个元素


<2>链表内指定区间反转

题目链接
描述:

将一个节点数为 size 链表 m 位置到 n 位置之间的区间反转,要求时间复杂度 O(n),空间复杂度 O(1)。
例如:
给出的链表为1→2→3→4→5→NULL, m=2,n=4
返回 1→4→3→2→5→NULL
数据范围: 链表长度 0 < size ≤ 1000,0 < m ≤ n ≤ size,链表中每个节点的值满足∣val∣ ≤ 1000
要求:时间复杂度 O(n) ,空间复杂度 O(n)
进阶:时间复杂度 O(n),空间复杂度 O(1)

示例1:

输入:{1,2,3,4,5},2,4
返回值:{1,4,3,2,5}

示例2:

输入:{5},1,1
返回值:{5}

思路分析:

①头插法

思路大体可以分为三个步骤
1️⃣ 先创建一个前序头指针,为了防止第一个元素也要逆序。
2️⃣用双指针prev和cur找到m的位置,prev就指向m的前一个位置,方便把cur的下一个元素移动到cur的前面
3️⃣对于从m到n这些个位置的节点,依次断掉指向后续的指针,反转指针方向。

这个方法的本意是通过cur的移动把所有后续元素移动到cur的前一个
如图所示:
在这里插入图片描述

struct ListNode* reverseBetween(struct ListNode* head, int m, int n ) {
    //表头
    struct ListNode* pNewHead = (struct ListNode*)malloc(sizeof(struct ListNode));
    pNewHead->next = head;
    //前序节点
    struct ListNode* prev = pNewHead;
    //当前节点
    struct ListNode* cur = head;
    //找m
    for(int i = 1; i < m; i++)
    {
        prev = cur;
        cur = cur->next;
    }
    for(int i = m; i < n; i++)
    {
        struct ListNode* tmp = cur->next;
        cur->next = tmp->next;
        tmp->next = prev->next;
        prev->next = tmp;
    }
    head = pNewHead->next;
    free(pNewHead);
    pNewHead = NULL;
    return head;
}

②递归法

m == 1时,就是反转前n个元素,当m != 1时,我们把head的索引看作 1,那么如果把head->next节点的索引看作 1,那么相对于head->next就是从m - 1的位置开始反转。

再看当n == 1时,就是反转第一个元素,如果不是就往下递归,总能递归到n == 1
做法:
1️⃣先定义一个全局变量tmp,找到递归到第n个节点时,指向其后一个位置,然后把要反转的首节点(反转后的尾)连接到tmp
2️⃣递归找到首个反转的节点(m == 1)
3️⃣递归反转前n个节点,每个子问题的节点都连接tmp

还是用第一个例子来举例,当递归到最后一个子问题时:
在这里插入图片描述
然后前面的 2 同理也会链接到tmp。

static struct ListNode* tmp;

struct ListNode* reverse(struct ListNode* head, int n)
{
    //只颠倒第一个
    if(n == 1)
    {
        tmp = head->next;
        return head;
    }
    struct ListNode* node = reverse(head->next, n - 1);
    head->next->next = head;
    head->next = tmp;
    return node;
}

struct ListNode* reverseBetween(struct ListNode* head, int m, int n ) {
    if(m == 1)
    {
        return reverse(head, n);
    }
    struct ListNode* node = reverseBetween(head->next, m - 1, n - 1);
    head->next = node;
    return head;
}

<3>链表中的节点每k个一组翻转

题目链接
描述:

将给出的链表中的节点每 k 个一组翻转,返回翻转后的链表
如果链表中的节点数不是 k 的倍数,将最后剩下的节点保持原样
你不能更改节点中的值,只能更改节点本身。

数据范围:0 ≤ n ≤ 2000,1 ≤ k ≤ 2000,链表中每个元素都满足 0 ≤ val ≤ 1000,要求空间复杂度 O(1),时间复杂度 O(n)。
例如:给定的链表是 1→2→3→4→5
对于 k = 2 , 你应该返回 2→1→4→3→5
对于 k = 3 , 你应该返回 3→2→1→4→5

示例1:

输入:{1,2,3,4,5},2
返回值:{2,1,4,3,5}

示例2

输入:{},1
返回值:{}

思路分析:

①头插法

跟上面一个题类似,头插的方法也相同,不做过多赘述,要注意的是判断end是否超过结尾。

struct ListNode* reverseKGroup(struct ListNode* head, int k ) {
    struct ListNode* newHead = (struct ListNode*)malloc(sizeof(struct ListNode));
    newHead->next = head;
    struct ListNode* end = head, *prev = newHead;
    while(end)
    {
        int i = 1;
        for(i = 1; i < k && end != NULL; i++)
        {
            end = end->next;
        }
        if(end == NULL)
            break;
        int j = 1;
        for(j = 1; j < k; j++)
        {
            struct ListNode* tmp = head->next;
            head->next = tmp->next;
            tmp->next = prev->next;
            prev->next = tmp;
        }
        prev = head;
        head = head->next;
        end = head;
    }
    head = newHead->next;
    free(newHead);
    newHead = NULL;
    return head;
}

②递归法

把每一段分开逆序,每一段的逆序前面已经做过,那么接下来就是把每段逆序后的结果链接到一起。
要注意的是:
例如第一段逆序完后head为尾部,那么链接下一段就是head->next = 子问题
结束的条件是当end走到NULL。

struct ListNode* _reverseKGroup(struct ListNode* head, struct ListNode* end)
{
    if(head == end || head->next == end)
    {
        return head;
    }
    struct ListNode* tmp = _reverseKGroup(head->next, end);
    head->next->next = head;
    head->next = end;
    return tmp;
}

struct ListNode* reverseKGroup(struct ListNode* head, int k ) {
    struct ListNode* end = head;
    for(int i = 1; i < k && end; i++)
    {
        end = end->next;
    }
    if(end == NULL)
        return head;
    //用ret接收头部
    struct ListNode* ret = _reverseKGroup(head, end->next);
    //head为尾,链接下一段
    head->next = reverseKGroup(head->next, k);
    return ret;
}

四、小结

链表的题目一定要画图,如果递归不知道怎么写就分析最后一个子问题情况,问题就可以迎刃而解。递归的经典例题在之前的文章也有讲解:
递归经典题目详解