【数据结构】10道经典面试题目带你玩转链表

简介: 【数据结构】10道经典面试题目带你玩转链表

一.移除链表元素

题目链接:

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


题目描述:

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点


题目详情:


解题思路:

思路一:双指针删除val法

如图,我们创建两个指针,分别是prev和cur,在初始时,它们一个指向空,一个指向链表的第一个结点(即head):

然后让两指针顺着链表向后移动,如果碰到cur不是val的值的结点就继续同时向后移动:

如果在移动的过程中碰到了cur是val的值的结点则将prev链上cur的下一个结点,并将cur结点释放:

删除后prev不动,cur继续向后移动:

如果cur指向的结点不是val的值,则两个指针继续向后移动:

直到cur再次碰到值等于val的结点:

继续将prev链上cur的下一个结点,并将cur结点释放:

删除后prev不动,cur继续向后移动:

这时发现cur已经走到NULL,则链表已经遍历到尾,即已删除完毕,向主函数返回此时链表的头指针head即可.


思路二:双链表遍历尾插法

如图,我们再创建一个单链表newhead,然后创建一个cur指针负责遍历待删链表,再创建一个tail指针负责记录新链表的尾结点:

当cur结点的值不为val时,我们将该结点尾插到新链表的后面:

然后cur指针继续向后移动遍历旧链表:

碰到cur结点的值不为val时,我们继续将该结点尾插到新链表的后面:

然后cur指针向后走,tail指针同样要重新指向新链表的尾结点:

当cur结点的值为val时,我们创建一个新指针next记录下cur的next结点,然后将cur结点free掉,再使cur指针指向刚刚记录的next结点.

然后cur继续向后遍历,遇到值为val的结点就删除,遇到非val的结点就尾插到新结点:

注意,当最后一个结点也是我们要删除的结点时,我们在删除结束后记得要将tail的指针域置为空,否则会导致新链表的尾结点末端连着一个非法空间!

如上,删除结束后返回新链表的头指针newhead即可.


解题代码:

思路一解题代码:

//双指针删val法
struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode*cur=head;
    struct ListNode*prev=NULL;
    while(cur)
    {
        if(cur->val != val)
        {
            prev=cur;
            cur=cur->next;
        }
        else
        {
            if(prev == NULL) //如果是头删,则更新头指针,然后删除头结点
                             //否则会造成prev空指针解引用问题
            {
                head=cur->next;
                free(cur);
                cur=head;
            }
            else
            {
                prev->next=cur->next;
                free(cur);
                cur=prev->next;
            }
        }
    }
    return head;
}

提交运行:



思路二解题代码:

//双链表遍历尾插法
struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode*cur=head;
    struct ListNode*newhead=NULL;
    struct ListNode*tail=NULL;
    while(cur)
    {
        if(cur->val!=val)
        {
            //尾插
            if(newhead==NULL)//头插是赋值
            {
                newhead=tail=cur;
            }
            else
            {
                tail->next=cur;
                tail=tail->next;
            }
            cur=cur->next;
        }
        else
        {
            struct ListNode*next=cur->next;
            free(cur);
            cur=next;
        }
    }
    if(tail!=NULL)//防止最后一个位置是待删元素,free后新链表的尾结点的指针域是野指针问题.
    {
        tail->next=NULL;
    }
    return newhead;
}

提交运行:


二.反转链表

题目链接:

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


题目描述:

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。


题目详情:


解题思路:

思路一:三指针逆连链表法

如图,我们创建三个指针分别是p1,p2,p3.使它们分别指向首结点,第二结点,第三结点:

然后将p2指针的next指向p1:

然后使p1,p2,p3向前挪动:

再将p2指针的next指向p1:

然后p1,p2,p3继续向前挪动:

再将p2指针的next指向p1:

再将p1,p2,p3继续向前挪动:

再将p2指针的next指向p1:

再将p1,p2,p3继续向前挪动:

可以看到,当p2指针为空时,链表已经全部逆链完毕,这时返回p1指针即为逆转后的链表头.


思路二:双链表取结点头插法

如图,我们创建一个新链表newhead,以及一个用来遍历待逆置链表的指针cur:

然后使cur向后遍历,每找到一个结点就将其头插到newnode链表中:

继续向后移动然后头插到newnode中:

直到将待逆置链表全部尾插到newhead链表中:

这时链表就逆置完毕了,这时返回newhead,即新链表的头即可.


解题代码:

思路一解题代码:

//三指针逆链表法
struct ListNode* reverseList(struct ListNode* head)
{
    if(head==NULL)
    {
        return NULL;
    }
    struct ListNode*p1=NULL;
    struct ListNode*p2=head;
    struct ListNode*p3=head->next;
    if(p3==NULL)
    {
        return head;
    }
    else if(p3->next==NULL)
    {
        p3->next=p2;
        p2->next=p1;
        return p3;
    }
    else
    {
        p1=p2;
        p2=p2->next;
        p1->next=NULL;
        while(p2)
        {
            p3=p3->next;
            p2->next=p1;
            p1=p2;
            p2=p3;
        }
        return p1;
    }
}

提交运行:


思路二解题代码:

//取结点头插法
struct ListNode* reverseList(struct ListNode* head)
{
    struct ListNode*newnode=NULL;
    struct ListNode*cur=head;
    while(cur)
    {
        if(newnode==NULL)//当newnode为NULL时单独赋值处理,并把尾结点置空
        {
            newnode=cur;
            cur=cur->next;
            newnode->next=NULL;
        }
        else
        {
            struct ListNode*prev=newnode;
            newnode=cur;
            cur=cur->next;
            newnode->next=prev;
        }
    }
    return newnode;
}

提交运行:


三.链表的中间结点

题目链接:

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


题目描述:

给你单链表的头结点 head ,请你找出并返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。


题目详情:


解题思路:

快慢指针解题法:

如图,我们分别创建两个指针,一个快指针,一个慢指针,开始时它们都指向链表的头结点:

然后我们让两指针循环向后遍历链表,但我们每次让fast指针向后走两步,而slow指针只向后走一步,如下是两个指针前进一次的样子:

如此循环前进,直到fast走到链表的最后一个结点,即fast->next=NULL时:

可以看到,当fast走到尾时,slow指针恰好指向链表的中间结点.

当然,当链表的结点数为偶数时,则当fast走到NULL时,slow指针恰好指向链表的中间结点,如:


解题代码:

//快慢指针解题法
struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode*fast=head;
    struct ListNode*slow=head;
    while(fast&&fast->next)
    {
        fast=fast->next->next;
        slow=slow->next;
    }
 
    return slow;    
}

提交运行:


四.链表中倒数第K个结点

题目链接:

https://www.nowcoder.com/share/jump/1020746871700211094739


题目描述:

输入一个链表,输出该链表中倒数第k个结点。


题目详情:


解题思路:

快慢(先后)指针法:

如图,我们设置两个指针,一个是fast,一个是slow,开始时它们分别指向链表的头结点:

在开始时,我们求链表中的倒数第k个结点,就先让fast向前走k步,如我们要求上图链表中的倒数第2个结点,则我们先让fast指针向前走2步:

当fast走完k步后,fast开始和slow一起向后挪动,直到fast走到NULL为止:

一起向后走一步:

一起向后再走一步:

当fast走到NULL时,slow恰好指向链表的倒数第二个结点:

这时我们在函数中返回slow指针即可.


解题代码:

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
    //快慢指针法
    struct ListNode*fast=pListHead;
    struct ListNode*slow=pListHead;
    while(k)
    {
        if(fast==NULL)//防止k的长度比链表的长度长
        {
            return NULL;
        }
        fast=fast->next;
        k--;
    }
    while(fast)
    {
        fast=fast->next;
        slow=slow->next;
    }
    return slow;
}

提交运行:


五.合并两个有序链表

题目链接:

https://leetcode.cn/problems/merge-two-sorted-lists/


题目描述:

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。


题目详情:


解题思路:

双指针尾插新链表法:

如图,我们创建一个新链表,以及三个指针,分别是cur1,cur2,tail,分别用来遍历链表1,遍历链表2,记录新链表的尾结点.

然后我们比较cur1和cur2的值,将其中的较小者尾插到newhead链表中(假设两个值相同时我们默认将cur1插入到链表中):

然后分别更新cur1指针和tail指针,使它们分别指向下一个结点和新链表的尾结点:

然后再比较cur1和cur2的值,将它们的较小者插入到新链表中:

再更新cur2指针和tail指针:

直到某一链表的结点全部插入到newhead中,如:

这时将不为空的链表剩下的全部结点直接链接在tail指针后面即可:

这时得到的新链表就是两个链表合并为升序链表的结果,我们返回newhead即可.


解题代码:

//双指针比较尾插新链表法
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    struct ListNode*cur1=list1;
    struct ListNode*cur2=list2;
    struct ListNode*newhead=NULL;
    struct ListNode*tail=newhead;
    while(cur1&&cur2)
    {
        if(cur1->val<=cur2->val)
        {
            if(newhead==NULL)//首结点是赋值
            {
                newhead=cur1;
                tail=newhead;
            }
            else
            {
                tail->next=cur1;
                tail=tail->next;
            }
            cur1=cur1->next;
        }
        else
        {
            if(newhead==NULL)//首结点是赋值
            {
                newhead=cur2;
                tail=newhead;
            }
            else
            {
                tail->next=cur2;
                tail=tail->next;
            }
            cur2=cur2->next;
        }
    }
    if(cur1==NULL)
    {
        if(tail)//防止tail为NULL时的空指针解引用问题
        {
            tail->next=cur2;
        }
        else
        {
            return cur2;
        }
        
    }
    else
    {
        if(tail)
        {
            tail->next=cur1;
        }
        else
        {
            return cur1;
        }
    }
    return newhead;
}

提交运行:


六.链表分割

题目链接:

https://www.nowcoder.com/share/jump/1020746871700135186269


题目描述:

现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。


题目详情:


解题思路:

双链表分类再合并法:

如图,我们创建一个cur指针用来遍历链表,然后创建一个新链表newhead1来存放比x小的结点,同时创建一个tail1指针记录这个链表的尾结点.

再创建一个新链表newhead2来存放比x大的结点,同时创建一个tail2指针记录这个链表的尾结点:

我们以x等于3为例,如果cur指向的结点的val值小于3,就将该结点尾插到newhead1链表中,反之,如果cur指向的结点的val值大于等于3,就将该结点尾插到newhead2链表中:

我们将cur指向的'1'结点尾插到newhead1中,并用tail1记录下newhead1的尾结点,然后cur向后遍历:

再将'2'结点尾插到newhead1链表中,更新tail1和cur的指向:

这时cur指向的结点的val值为6,大于x,我们把它尾插到newhead2中,更新cur和tail2的值:

将后续结点依次判断后尾插到newhead1或newhead2中去,直到cur为NULL:

当将原链表中的结点全部分到两个新链表中后,将newhead1和newhead2链表重新链接在一起,即tail1的指针域连接上newhead2,就可以得到一个新的按题目要求排序好的新链表了:

这时返回新链表的头,即newhead1给函数即可.


解题代码:

//双链表分类再合并法
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x)
    {
        struct ListNode*cur=pHead;
        struct ListNode*newhead1=NULL;
        struct ListNode*tail1=NULL;
        struct ListNode*newhead2=NULL;
        struct ListNode*tail2=NULL;
        while(cur)
        {
            if(cur->val<x)
            {
                if(newhead1==NULL)
                {
                    newhead1=cur;
                    tail1=newhead1;
                }
                else 
                {
                    tail1->next=cur;
                    tail1=tail1->next;
                }
            }
            else
            {
                if(newhead2==NULL)
                {
                    newhead2=cur;
                    tail2=newhead2;
                }
                else 
                {
                    tail2->next=cur;
                    tail2=tail2->next;
                }
            
            }
            cur=cur->next;
        }
        if(tail2!=NULL)//防止tail2尾结点的指针域不为空造成新链表带环
        {
            tail2->next=NULL;//所以要将tail2的指针域置为空
            if(tail1!=NULL)
            {
                tail1->next=newhead2;//置空之后链接两个链表
                return newhead1;    //链接成功返回newhead1
            }
            else
            {
                return newhead2;   //tail1==NULL意味newhead1没有元素,则可以直接返回newhead2
            }
        }
        else     //tail2==NULL意味newhead2没有元素,则可以直接返回newhead1
        {
            return newhead1;
        }
        
        return newhead1;
    }
};

提交运行:


七.链表的回文结构

题目链接:

https://www.nowcoder.com/share/jump/1020746871700305446248


题目描述:

对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。

给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

如:

1->2->2->1

返回:

true

题目详情:


解题思路:

先逆置后对比法:

思路为:我们先将原链表的后半段逆置,然后再将其和原链表的前半段作对比,如果相同,则是回文结构.

实现该思路我们需要先找中间结点,再逆置中间结点后的链表部分,再将逆置后的链表和原链表的前半部分做对比:

图示如下,找中间结点:

逆置中间结点后的链表得到newhead:

逐一对比head链表和newhead链表的结点,如果有不一样的,则不是回文结构,如果都一样,则是回文结构:

注:查找中间结点以及逆转链表的代码思路我们在前面几道题中已经实现过了,在这里直接套用即可.


解题代码:

//先逆转再对比法
ListNode*midnode(ListNode*head)//找中间结点函数
{
    struct ListNode*fast=head;
    struct ListNode*slow=head;
    while(fast&&fast->next)
    {
        fast=fast->next->next;
        slow=slow->next;
    }
    return slow;
}
 
ListNode* reverseList(struct ListNode* head)//逆转函数
{
    struct ListNode*newnode=NULL;
    struct ListNode*cur=head;
    while(cur)
    {
        if(newnode==NULL)//当newnode为NULL时单独赋值处理,并把尾结点置空
        {
            newnode=cur;
            cur=cur->next;
            newnode->next=NULL;
        }
        else
        {
            struct ListNode*prev=newnode;
            newnode=cur;
            cur=cur->next;
            newnode->next=prev;
        }
    }
    return newnode;
}
 
 
class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        //找中间结点   slow
    struct ListNode*mid=midnode(A);
 
        //逆置尾链表
    struct ListNode*newhead=reverseList(mid);
 
        //对比新链表
    struct ListNode*cur1=A;
    struct ListNode*cur2=newhead;
    while(cur1&&cur2)
    {
        if(cur1->val!=cur2->val)
        {
            return false;
        }
        cur1=cur1->next;
        cur2=cur2->next;
    }
    return true;
    }
};

提交运行:


八.相交链表

题目链接:

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


题目描述:

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

图示两个链表在节点 c1 开始相交

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构


题目详情:


解题思路:

先求长再找点法:

对于这道题,我们首先要理解:假设两个链表短的长度为x,长的长度为y,则他们的交点一定位于长链表的后x结点中,而不会位于后x之外的结点:

因为交点之后的结点就是两个链表共用的了,即相交后的链表元素如果A有那么B也一定有,因此不可能出现两链表的交集比某一链表的长度还长的情况.

所以我们在找交点时应该从长链表的第y-x个结点和短链表的第一个结点开始一起向后找有没有相同的结点:

如果有,则为两链表的交点,如果没有,则两链表没有交点:

因此我们本题的思路是,先分别求出A,B链表的长度,然后让长链表向后走到后面只有x个结点的地方,开始和短链表一起向后移动,在移动的过程中找有没有交点,如果找到了,返回交点,如果走到尾还没有找到,则返回0.


解题代码:

//同长后移找交点法
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
    //先遍历A,B链表求出长度
    struct ListNode*cur1=headA;
    struct ListNode*cur2=headB;
    int len1=0;
    int len2=0;
    while(cur1)
    {
        cur1=cur1->next;
        len1++;
    }
    while(cur2)
    {
        cur2=cur2->next;
        len2++;
    }
    int k=abs(len1-len2);
 
    //让长的先走到和短的长度一样的地方,然后开始同时向后走
    cur1=headA;
    cur2=headB;
    if(len1>len2)
    {
        while(k--)
        {
            cur1=cur1->next;
        }
        while(cur1&&cur2)
        {
            if(cur1==cur2)
            {
                return cur1;
            }
            cur1=cur1->next;
            cur2=cur2->next;
        }
        return 0;
    }
    else
    {
        while(k--)
        {
            cur2=cur2->next;
        }
        while(cur1&&cur2)
        {
            if(cur1==cur2)
            {
                return cur2;
            }
            cur1=cur1->next;
            cur2=cur2->next;
        }
        return 0;
    }
}

提交运行:


九.环形链表

题目链接:

https://leetcode.cn/problems/linked-list-cycle/


题目描述:

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false


题目详情:


解题思路:

快慢指针相遇法:

即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,如果链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾.

现实中参考陪女朋友在操场跑步减肥时套圈的情景.


解题代码:

//双指针循环追逐法
bool hasCycle(struct ListNode *head)
{
    if(head==NULL||head->next==NULL)
    {
        return false;
    }
    struct ListNode*fast=head;
    struct ListNode*slow=head;
    while(fast&&fast->next)
    {
        fast=fast->next->next;
        slow=slow->next;
        if(fast==slow)
        {
            return true;
        }
    }
    return false;
}

提交运行:


十.环形链表找入环点

题目链接:

https://leetcode.cn/problems/linked-list-cycle-ii/


题目描述:

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。


题目详情:


解题思路:

思路一:逻辑等式求点法

如图,我们设起始点到入环点的距离为L,入环点到相遇点的距离为X,环长为C,则可有以下推导:

那么我们可以得到一个结论:如果一个指针从起始点开始走,一个指针从相遇点开始走,那么他们会在入环点相遇.

因此本题的思路为:先判断有环无环,有环的情况下创建一个指针cur从head开始走,然后让另一个指针从相遇点开始走,直到他们再次相遇,返回第二次的相遇点即为入环点.


思路二:断链求两链表相交法

思路如下:


解题代码:

思路一:

//思路一:逻辑等式求点法
struct ListNode *detectCycle(struct ListNode *head) 
{
    if(head==NULL||head->next==NULL)
    {
        return false;
    }
    struct ListNode*fast=head;
    struct ListNode*slow=head;
    while(fast&&fast->next&&slow)
    {
        fast=fast->next->next;
        slow=slow->next;
        if(fast==slow)
        {
            //链表有环,且相遇
            struct ListNode *cur=head;
            while(cur!=fast)
            {
                cur=cur->next;
                fast=fast->next;
            }
            return cur;
        }
    }
 
    //链表无环
    return NULL;
}

提交运行:

思路二:

struct ListNode *detectCycle(struct ListNode *head) {
    if(head==NULL||head->next==NULL)
    {
        return false;
    }
    struct ListNode*fast=head;
    struct ListNode*slow=head;
    while(fast&&fast->next&&slow)
    {
        fast=fast->next->next;
        slow=slow->next;
        if(fast==slow)
        {
            //链表有环,且相遇
            struct ListNode *cur=slow->next;
            slow->next=NULL;
            struct ListNode*cur1=cur;
            struct ListNode*cur2=head;
            int len1=0;
            int len2=0;
            while(cur1)
            {
                cur1=cur1->next;
                len1++;
            }
            while(cur2)
            {
                cur2=cur2->next;
                len2++;
            }
            int k=abs(len1-len2);
            //让长的先走到和短的长度一样的地方,然后开始同时向后走
            cur1=cur;
            cur2=head;
            if(len1>len2)
            {
                while(k--)
                {
                    cur1=cur1->next;
                }
                while(cur1&&cur2)
                {
                    if(cur1==cur2)
                    {
                        return cur1;
                    }
                    cur1=cur1->next;
                    cur2=cur2->next;
                }
            }
            else
            {
                while(k--)
                {
                    cur2=cur2->next;
                }
                while(cur1&&cur2)
                {
                    if(cur1==cur2)
                    {
                        return cur2;
                    }
                    cur1=cur1->next;
                    cur2=cur2->next;
                }
            }
            
        }
    }
    //链表无环
    return NULL;
    
}

提交运行:


结语

希望通过上面的题目能使大家对链表这一经典数据结构的理解以及运用能够更上一层楼,欢迎大佬们留言或私信与我交流.学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!


相关文章
|
2天前
|
存储
数据结构第二课 -----线性表之单向链表
数据结构第二课 -----线性表之单向链表
|
1天前
|
Web App开发 JavaScript Android开发
webRTC(十五),2024最新Android中级面试题目汇总解答
webRTC(十五),2024最新Android中级面试题目汇总解答
|
1天前
|
前端开发 JavaScript 开发工具
4(1),阿里面试官,前端开发面试题目
4(1),阿里面试官,前端开发面试题目
|
1天前
|
JavaScript 前端开发 Go
经典面试题目
经典面试题目
10 0
|
1天前
|
存储
数据结构链表详解(不仅顺序表可以,我链表也可以)
数据结构链表详解(不仅顺序表可以,我链表也可以)
10 0
|
2天前
|
存储 算法 Java
数据结构与算法 数组和链表
数据结构与算法 数组和链表
11 0
|
2天前
|
存储 Java
深入浅出数据结构之链表
深入浅出数据结构之链表
|
2天前
|
C++
数据结构(双链表
数据结构(双链表
10 1
|
2天前
|
存储 缓存
[数据结构]~双向+循环链表从(0~1)
[数据结构]~双向+循环链表从(0~1)
|
2天前
|
存储
数据结构第三课 -----线性表之双向链表
数据结构第三课 -----线性表之双向链表