链表经典面试题

以下的每个题都配有链接,大家可以点击进入进行练习,想了解更多知识可以关注以下个人主页

敲上瘾-优快云博客

目录

1.反转链表

2.回文链表

3.相交链表

4.环形链表

5.合并两个有序链表


1.反转链表

        分析:首先确定函数的返回值,需要返回的是反转后的头节点,显然,返转后的头结点是原链表的尾结点,而原链表的头结点应该指向NULL。

        方法一:使用一个结构体指针数组,将每一个结点存入数组内,然后把arr[0]的下一个结点指向NULL,arr[1]的下一个结点指向arr[0],以此类推。然后把数组最后一个元素返回。此方法空间复杂度为O(n),时间复杂度为O(n)

        方法二:这个题就是让我们把头结点的next指向NULL,然后把后面的每个结点的next都指向前一个结点,可以使用三个指针变量比如n1,n2,n3。n1=NULL,n2=head,n3=head->next。n2存放的是需要改变的结点,n1存放的是前一个结点(防止前结点找不到),n3存放后一个结点(防止后结点丢失),然后每完成一次反转,这三个指针同时往右移一位空间复杂度为O(1),时间复杂度为O(n)。如下图:

代码演示: 

struct ListNode* reverseList(struct ListNode* head) {
    if(head==NULL||head->next==NULL)
        return head;
    struct ListNode* n1=NULL,*n2=head,*n3=head->next;
    while(n2)
    {
        n2->next=n1;
        n1=n2;
        n2=n3;
        if(n3)
            n3=n3->next;
    }
    return n1;
}

2.回文链表

        分析:回文链表的特性是对称性,也就是它关于中间结点对称,即链表节点序列从后往前看和从前往后看是相同的,实例1从前往后看和从后往前看都是1,2,3,3,2,1。是一个回文链表。 

        方法一:使用结构体指针数组储存各节点(也可用整型数组直接储存各节点对应的值),然后从最左边和最右边开始比较它们对应的值,然后两边同时往中间移,直到比较到最中间元素都相等为止,则为回文链表。空间复杂度为O(n),时间复杂度为O(n)。因为该方法空间复杂度为O(n),效率比较低,在这里就不在展示,来看方法二。

        方法二:因为回文链表具有对称性,所以把链表从中间分为两段,后半段反转后是和前半段相同的。所以现在要做的是把后半段反转,首先就需要找到中间节点,如何来找呢?,有一个巧妙的方法,就是用快慢指针,快指针每次走两步,慢指针每次走一步。这样快指针走完整个链表,慢指针刚好走完链表的一半,此时慢指针指向的位置就是链表的中间节点。然后把该节点当做头节点进行反转变成新的链表最后与前半段进行比较。空间复杂度为O(1),时间复杂度为O(n)

struct ListNode* Fnod(struct ListNode* head)//反转链表
{
    struct ListNode* n1=NULL,*n2=head,*n3=head->next;
    while(n2)
    {
        n2->next=n1;
        n1=n2;
        n2=n3;
        if(n3)
            n3=n3->next;
    }
    return n1;
}
bool isPalindrome(struct ListNode* head) {
    if(!head)
        return false;
    if(head->next==NULL)
        return true;
    struct ListNode* p1=head,*p2=head;
    while(p2&&p2->next)//找中间节点
    {
        p1=p1->next;
        p2=p2->next->next;
    }
    struct ListNode* fp= Fnod(p1);
    while(fp&&head)//比较
    {
        if(fp->val!=head->val)
            return false;
        fp=fp->next;
        head=head->next;
    }
    return true;
}

3.相交链表

        方法一(暴力算法):用链表A的每一个元素与链表B比较一遍,如果出现相同的地方则链表相交,并返回该节点。空间复杂度为O(1),时间复杂度为O(nm)

        方法二(构造栈空间):我们观察到的第2个特点是如果两个链表有重合节点,那么这些重合节点一定只出现在链表的尾部。如果两个单向链表有重合节点,那么从某个节点开始这两个链表的next指针都指向同一个节点。在单向链表中,每个节点只有一个 next指针,因此在第1个重合节点开始之后它们的所有节点都是重合的,不可能再出现分叉。
        由于重合节点只可能出现在链表的尾部,因此可以从两个链表的尾部开始向前比较,最后一个相同节点就是我们要找的节点。但是在单向链表中,只能从头节点开始向后遍历,直至到达尾节点。最后到达的尾节点却要最先被比较,这就是通常所说的"后进先出"。至此不难想到可以用栈来解决这个问题:分别把两个链表的节点放入两个栈,这样两个链表的尾节点就位于两个栈的栈顶。接下来比较两个栈的栈顶节点是否相同。如果相同,则把栈顶节点弹出,然后比较下一个栈顶节点,直到找到最后一个相同的节点空间复杂度为O(n+m),时间复杂度为O(n+m)

        方法三(双指针):首先遍历两个链表得到它们的长度,比如长度差为n,则比较长的链表先走n步使它们长度对齐,然后两个链表再一起走,如果有交点的话它们必然会相遇。空间复杂度为O(1),时间复杂度为O(n+m)

        方法四(双指针):基于方法三还有一个更巧妙的方法,使用两个速度相同的指针p1和p2,指针p1从链表A跑完后跑链表B,指针p2从链表B跑完后跑链表A,每跑一次比较一次,因为它们路程相同,速度相同,当它们有节点的时候必然会相遇。空间复杂度为O(1),时间复杂度为O(n+m)。

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode*p1=headA,*p2=headB;
    while(p1!=p2)
    {
        p1=p1!=NULL?p1->next:headB;
        p2=p2!=NULL?p2->next:headA;
    }
    return p1;
}

4.环形链表

        分析:首先我们用快慢指针来判断是否有环,快指针p2每次走两步,慢指针p1每次走一步,如果有环他们必然相遇。看似很显然,不过我们依然需要证明一下,进行深入理解(在后面图中给出证明)。

        如果没有环,即快指针会先走到NULL,直接返回NULL即可。如果有环,我们求一下环的长度,即让快指针停留在他们相遇的位置,只让慢指针走,每走一步计一次数,直到他们再次相遇,所计的数即为环的长度,记为c。

        最后再用两个指针从头开始跑,第1个指针先跑c步以后,第二个指针再开始跑,因为环的长度为c,所以第2个指针跑完一圈环后,刚好回到环的入口点与第1个指针相遇。那么这样就找到环的入口点了。

        该方法可以把它优化一下,不用计算环的长度,当快慢指针相遇时,假设慢指针走了m步,那么快指针就走了2m步。那么它们的差值(即m)就是环长度的整数倍。现在相当于慢指针p1提前走了m步,只需要再找一个慢指针p3,从头节点开始,两慢指针同时走,到它们相遇时即为环的入口点。

        当然这个题我们不能止步于此,我们试着给出以上证明,并且分析更复杂的情况(即快指针一次走3步,4步,甚至更多可不可以解决这个问题)。

慢指针一次走1步,快指针一次走3步的情况(其它情况与这个类似分析,就不在赘述):

现在要考虑的是n为奇数c为偶数这种情况有没有可能不会同时出现?

答案是确实不会的,证明如下:

以上只是证明了用快慢指针来判断是否有环的问题。下面来证明环的入口的查找。

代码演示:

struct ListNode *detectCycle(struct ListNode *head) {
    if(!head||!head->next)
        return NULL;
    struct ListNode* s1=head,*s2=head,*k1=head;
    while(s2&&s2->next)
    {
        s1=s1->next;
        s2=s2->next->next;
        if(s1==s2)
        {
            while(k1!=s1)
            {
                k1=k1->next;
                s1=s1->next;
            }
            return k1;
        }
    }
    return NULL;
}

5.合并两个有序链表 

        方法一:使用结构体指针数组将它们有序的储存到同一个数组当中,然后在把数组的从头至尾的元素链接在一起。

         方法二:创建一个新的结构体指针变量,然后遍历两个链表,将两个链表中的节点有序的连接到新创建的节点中,构成一个新的带头链表,然后返回头结点的next。

typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    if(list1==NULL||list2==NULL)
        return list1==NULL?list2:list1;
    ListNode* ph=(ListNode*)malloc(sizeof(ListNode));
    ListNode* head=ph;
    while(list1&&list2)
    {
        if(list1->val<=list2->val)
        {
            ph->next=list1;
            list1=list1->next;
        }
        else
        {
            ph->next=list2;
            list2=list2->next;
        }
        ph=ph->next;
    }
    if(list1)
        ph->next=list1;
    if(list2)
        ph->next=list2;
    ph=head->next;
    free(head);
    return ph;
}

        总结以上方法我们可以发现每个链表相关的题几乎都可以构造一个结构体指针数组来解决,确实这个方法逻辑简单,更容易被我们想到,不过这种空间复杂度为O(n)的算法,不足以拿到offer。只适合像暴力算法一样把它当做为最后的选择。

<think> 首先,用户查询是:“我想查找关于链表面试题 请问链表相关面试题”。用户想查找链表相关的面试题。 从系统指令中,我需要: - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如:[^1]。 - 结构清晰,帮助用户逐步解决问题。 - 参考提供的站内引用:[1], [2], [3], [4]。 这些引用包含链表相关的内容: - 引用[1]:涉及合并两个有序链表的代码(mergeTwoLists),并提到一个面试题“以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前”。 - 引用[2]:讨论链表中环的检测。 - 引用[3]:提到数据结构面试题,包括链表分割(第六题:链表分割)。 - 引用[4]:类似[1],有合并链表的代码和再次提到“编写代码,以给定值x为基准将链表分割成两部分”。 用户没有指定具体的链表问题,而是请求一般的链表面试题。所以,我应该提供一个链表面试题的集合,基于提供的引用。 结构化回答: 1. **介绍**:简要解释链表面试中的重要性。 2. **列出常见链表面试题**:基于引用整理相关问题。 - 从引用中提取的问题: - 合并两个有序链表(mergeTwoLists):这在引用[1], [3], [4]中都有代码。 - 链表分割:以给定值x分割链表,小于x的在前。[^1], [^3], [^4] - 检测链表中环的存在:[^2] - 其他常见链表问题(我可以添加,但要基于引用)。引用提到“链表经典面试题”和“数据结构 十一道链表相关面试题”,但具体内容没有列出。我应该专注于引用的内容。 引用[3]说“数据结构 十一道链表相关面试题”,但没有列出所有题。引用中只提到了合并链表链表分割。 所以,我列出明确的: - 合并两个有序链表 - 链表分割:以x为基准分割 - 检测链表是否有环 此外,我可以添加一些其他常见问题,如反转链表、找出中间节点、删除节点等,但引用的内容中没有这些。用户指令说“尽可能多的参考用户上一次的问题和你的回答”,但这是第一次交互,所以参考提供的引用。 用户消息开头是“参考站内引用”,所以我优先使用这些。 3. **提供示例代码**:如果可能,展示一些代码片段,但要简洁。 4. **生成相关问题**:在结尾添加相关问题,引导用户深入。 回答结构: - 开头:确认用户查询,介绍链表面试题。 - 主要内容:列出几个常见链表面试题,简要描述,并引用来源。 - 结尾:生成相关问题。 使用引用自然地添加标识:[^1], [^2], [^3], [^4]。 例如: - 对于链表分割问题,可以说:这是一个常见问题,要求以值x分割链表。[^1][^3][^4] 代码部分:引用中有代码,但我可以简要描述,而不是复制整个代码块。 在回答中,代码应该用代码块格式,如 ```c 或 ```java。 系统指令说“所有行内数学表达式必须使用$...$”,但这里是链表问题,可能不需要数学表达式,除非涉及计算。 现在,列出基于引用的面试题: 1. **合并两个有序链表**:合并两个升序链表为一个新的升序链表。这在引用[1], [3], [4]中都有代码实现。[^1][^3][^4] 2. **链表分割**:以给定值x为基准,将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前。这在引用[1], [3], [4]中提及。[^1][^3][^4] 3. **检测链表是否有环**:确定链表中是否存在环。引用[2]描述了这个问题。[^2] 此外,引用[3]提到“十一道链表相关面试题”,但没有列出所有题。我可以暗示有其他问题。 在生成相关问题中,我可以问关于这些。 完整回答: - 介绍:链表数据结构中常见主题,面试中经常考察。 - 列出问题: 1. 合并两个有序链表:描述 + 引用。 2. 链表分割:描述 + 引用。 3. 检测链表环:描述 + 引用。 4. 其他可能的问题如反转链表等,但引用的内容中没有,所以 stick to references. - 引用[3]提到“第六题 :链表分割”,所以有其他题,但未列出。我可以 generalise. 最好只覆盖引用的内容。 结尾:生成相关问题,如: - 如何实现反转链表? - 如何找出链表的中间节点? </think> ### 链表相关面试题概述 链表是一种常见的数据结构,在面试中经常被考察,因为它能有效测试候选人对指针操作、内存管理和算法设计的理解。链表面试题通常涉及基本操作(如插入、删除、遍历)以及更复杂的算法(如合并、分割、环检测)。基于提供的站内引用内容,以下是一些经典链表面试题,这些题目在牛客网、LeetCode等平台中高频出现。我会简要描述每个问题的核心要求,并提供关键思路或代码片段(基于引用中的实现)。代码示例使用C语言Java,以匹配引用内容。 #### 1. 合并两个有序链表 **问题描述**: 将两个升序链表合并为一个新的升序链表。新链表应通过拼接给定链表的节点来构建。 - **关键思路**: 使用哨兵节点(带头节点)简化边界处理,并用两个指针遍历原链表,比较节点值,较小的节点被尾插到新链表中。如果一个链表为空,则直接返回另一个链表。 - **代码示例 (C语言, 基于引用[1][^1]和引用[4][^4])**: ```c typedef struct ListNode ListNode; struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) { if (list1 == NULL) return list2; if (list2 == NULL) return list1; ListNode *newhead = (ListNode*)malloc(sizeof(ListNode)); // 创建哨兵节点 ListNode *newtail = newhead; ListNode *l1 = list1; ListNode *l2 = list2; while (l1 && l2) { if (l1->val < l2->val) { newtail->next = l1; newtail = newtail->next; l1 = l1->next; } else { newtail->next = l2; newtail = newtail->next; l2 = l2->next; } } if (l1) newtail->next = l1; // 处理剩余节点 if (l2) newtail->next = l2; ListNode *ret = newhead->next; free(newhead); // 释放哨兵节点 return ret; } ``` **参考理由**: 该方法高效且易于理解,时间复杂度为$O(n+m)$(n和m为链表长度),空间复杂度为$O(1)$。[^1][^4] #### 2. 链表分割(以给定值x为基准) **问题描述**: 以给定值x为基准,将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前。要求保持节点在原链表中的相对顺序。 - **关键思路**: 创建两个新链表(一个用于小于x的节点,一个用于大于或等于x的节点),遍历原链表分配节点,最后连接两个链表。注意避免使用额外空间或破坏原有顺序。 - **代码示例 (Java, 基于引用[3][^3]和引用[4][^4])**: ```java public class Solution { public ListNode partition(ListNode head, int x) { ListNode lessHead = new ListNode(-1); // 哨兵节点 for < x ListNode greaterHead = new ListNode(-1); // 哨兵节点 for >= x ListNode lessCur = lessHead; ListNode greaterCur = greaterHead; ListNode cur = head; while (cur != null) { if (cur.val < x) { lessCur.next = cur; lessCur = lessCur.next; } else { greaterCur.next = cur; greaterCur = greaterCur.next; } cur = cur.next; } lessCur.next = greaterHead.next; // 连接两个链表 greaterCur.next = null; // 防止环 return lessHead.next; } } ``` **参考理由**: 此解法在$O(n)$时间内完成,空间复杂度$O(1)$,且保持了节点顺序,适用于面试中的算法考察。[^1][^3][^4] #### 3. 检测链表是否有环 **问题描述**: 确定链表中是否存在环,并通过整数pos表示环的入口位置(索引从0开始)。评测系统内部使用pos标识环,但函数参数不包含pos。 - **关键思路**: 使用快慢指针(Floyd判圈算法)。快指针每次移动两步,慢指针每次移动一步,如果相遇则存在环。相遇后,重置一个指针到链表头,两个指针同步移动,再次相遇点即为环入口。 - **关键描述 (基于引用[2][^2])**: - 如果链表中有某个节点,可以通过连续跟踪next指针再次到达,则链表中存在环。 - 快慢指针法的时间复杂度为$O(n)$,空间复杂度为$O(1)$,是高效的标准解法。 **示例伪代码**: ```c bool hasCycle(struct ListNode *head) { if (head == NULL) return false; struct ListNode *slow = head; struct ListNode *fast = head; while (fast != NULL && fast->next != NULL) { slow = slow->next; fast = fast->next->next; if (slow == fast) return true; // 相遇表示有环 } return false; } ``` **参考理由**: 该问题测试对指针和循环的理解,是链表面试的基础题。[^2] #### 其他常见链表面试题提示 除了上述基于引用的问题外,链表面试中还有以下高频题(虽未在引用中详细展开,但常与这些题组合考察): - **反转链表**: 将链表逆序,时间复杂度$O(n)$。思路:使用迭代或递归调整指针方向。 - **找出链表中间节点**: 使用快慢指针,快指针到末尾时慢指针即为中间点。 - **删除链表节点**: 如删除指定值的节点或重复节点,需处理边界条件。 这些题目在LeetCode和牛客网有大量练习资源,建议结合平台刷题。 ###
评论 203
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

敲上瘾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值