一万五字的文章,超详细的画图,带你理解链表的基础和进阶题目(含快慢指针的讲解)

在今天的文章中,我将带来链表的面试题。在数据结构的学习过程中,画图是尤为重要的,所以在这些题目的讲解的过程中,我以画图为主。温馨提示:由于图片过大,手机观看可能出现模糊不清的情况,建议在电脑观看该篇文章(点击图片,Ctrl+鼠标滑轮看全图)。





1.移除链表的元素:链接

题目要求:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
请添加图片描述
如该题目的示例1,我们需要删除链表中的存储数值为6的结点,那么我们应该如何做呢?

在删除的过程中,头结点可能是我们要删除的目标,所以我们定义新的头结点NewHead,来更新链表。头结点NewHead赋值为NULL。
我们需要定义新的尾结点Ptail来更新新头结点的尾。尾结点Ptail赋值为NULL。
在删除链表的结点时,我们需要定义结点Next来存储要删除的结点的下一个结点的地址,以免找不到下一个结点。结点Next赋值为NULL。
我们要定义结点cur来遍历旧的链表。结点cur赋值为旧链表的头结点。

特殊情况:
当旧链表的最后一个结点被删除后,并且倒数第二个结点没有被删除,由于链表的next存储着下一个结点的地址,那么倒数第二个结点的next为野指针。因为旧链表倒数第一个结点被删除,所以旧链表倒数第二个结点就为新链表的尾指针,也就是赋值给Ptail,此时Ptail的next为野指针,那么Ptail的next要置为空指针。
当旧链表的所有的结点都要被删除时,那么Ptail一直没有被赋值,此时,Ptail的值为NULL,那么不能对Ptail进行解引用来修改next的值。(不能对空指针进行解引用)

思路1:
请添加图片描述
代码:

struct ListNode* removeElements(struct ListNode* head, int val)
{
    if(head == NULL)
        return NULL;
    struct ListNode* NewHead = NULL,*Ptail = NULL;
    struct ListNode* cur = head;
    while(cur != NULL)
    {
        struct ListNode* Next = cur->next;
        if(cur->val != val)                   //判断是否为要删除的结点
        {
            if(Ptail == NULL)                 //第一次对头指针NewHead和尾指针Ptail进行赋值
            {
                NewHead = Ptail = cur;
            }
            else                              //修改尾指针Ptail的next值和尾指针Ptail向后走
            {
                Ptail->next = cur;
                Ptail = Ptail->next;
            }
            cur = Next;
        }
        else                                 //删除结点
        {
            free(cur);
            cur = Next;
        }
    }
    if(Ptail)                               //如果Ptail不是空指针,那么将Ptail的next值改为NULL,防止野指针的存在
        Ptail->next = NULL;
    return NewHead;
}

思路2:

在前面的想法下,我进行改进,将NewHead、ptail指向NULL改为指向哨兵位,在加上哨兵位后,我们可以直接在哨兵位尾插结点,不用在判断Ptail是不是指向空指针后才进行尾插。
在前面的想法中,我们需要将尾结点Ptail的next置空,然而可能由于原来的链表的所有结点都被删除了,Ptail没有变化,一直指向空指针,所以在将尾结点的next置空前,需要判断Ptail是不是指向空指针。在加上哨兵位后,我们就不需要判断了,因为Pail一开始指向哨兵位,而不是空指针。

在加上哨兵位后,代码将被大大的优化。

请添加图片描述

代码:

struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode* cur = head;
    struct ListNode* guard,*Ptail;
    guard = Ptail = (struct ListNode*)malloc(sizeof(struct ListNode));
    while(cur != NULL)
    {
        if(cur->val != val)
        {
            Ptail->next = cur;
            Ptail = Ptail->next;
            cur = cur->next;
        }
        else
        {
            struct ListNode* Next = cur->next;
            free(cur);
            cur = Next;
        }
    }
    Ptail->next = NULL;
    struct ListNode* NewHead = guard->next;     //返回头结点,不要返回哨兵位
    free(guard);
    return NewHead;    
}     


2.反转链表: 链接

题目要求:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
请添加图片描述
如该题目的示例1,我们需要将把整个链表的结点给逆转过来。

思路1:

我们需要定义一个翻转链表后的头指针Rhead,初始化为NULL。
定义一个结点cur,遍历翻转前的链表。
定义一个结点next,存储着curr的下一个结点的地址。

请添加图片描述

代码:

struct ListNode* reverseList(struct ListNode* head)
{
    struct ListNode* cur = cur = head,*Rhead = NULL;
    while(cur != NULL)
    {
        struct ListNode* Next = cur->next;
        cur->next = Rhead;
        Rhead = cur;
        cur = Next;
    }
    return Rhead;
}

思路2:

定义三个结点n1、n2、n3,分别指向翻转前的链表的空指针、第一个结点、第二个结点,结点n3存储着结点n2的下一个结点,然后将结点n2的next值改为n1的地址,三个结点向后走,循环下去,直到结点n2指向空指针。

请添加图片描述
代码:

struct ListNode* reverseList(struct ListNode* head)
{
    if(head == NULL)                     //判断头指针非空,不然初始化n3时,存在着对空指针解引用的问题
        return NULL;
    struct ListNode* n1,*n2,*n3;
    n1 = NULL;
    n2 = head;
    n3 = n2->next;
    while(n2 != NULL)
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if(n2 != NULL)
            n3 = n2->next;
    }
    return n1;
}


3.链表的中间结点链接

题目要求:给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。

请添加图片描述
如该题的示例1,我们找到中间结点,即存储的值为3的结点,将它做为新头指针并返回,那么我们应该怎么做呢?

使用快慢指针,定义两个指针分别是fast、slow,指针slow和指针fast都指向链表的头结点,指针slow每次往后走一步,指针fast每次往后走两步,当指针fast的next为NULL或者指针fast为NULL,指针slow分别指向中间结点或者第二个中间结点(在中间结点的个数为2个的时候)。

链表结点的个数为奇数时(链表只有一个中间结点):

请添加图片描述

链表的结点个数为偶数时(链表有两个中间结点):
请添加图片描述
代码:

struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* slow,*fast;
    slow = fast = head;
    while(fast != NULL && fast->next != NULL)
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}


4.链表中倒数第k个结点:链接

题目要求:输入一个链表,输出该链表中倒数第k个结点。
请添加图片描述
如示例1,我们需要找到链表的倒数第1个结点,也就是存储的数值为5的结点。

思路1:依然使用快慢指针,让快指针fast先走k步,再让快指针fast和慢指针slow一起往后走,当快指针fast指向空指针时,慢指针slow刚好指向链表中倒数第k个结点。

特殊情况:k可能过大,超过了链表的长度,导致快指针往后走的距离大于链表的长度,造成越界。

假设我要找到倒数第二个结点。
请添加图片描述
代码:

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
    if(pListHead == NULL)
        return NULL;
    struct ListNode* slow,*fast;
    slow = fast = pListHead;
    while(--k)                    //k--总共循环k次,快指针fast往后走k步
    {
        fast = fast->next;
        if(fast == NULL)          //k的步长大于链表的长度
            return NULL;      
    }
    while(fast->next != NULL)
    {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}

思路2:依然使用快慢指针,让快指针fast先走k-1步,再让快指针fast和慢指针slow一起往后走,当快指针fast的next指向空指针时,慢指针slow刚好指向链表中倒数第k个结点。

假设我要找到倒数第二个结点。

请添加图片描述
代码:

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
    if(pListHead == NULL)
        return NULL;
    struct ListNode* slow,*fast;
    slow = fast = pListHead;
    while(--k)                       //--k总共循环k-1次,快指针fast往后走k-1步
    {
        fast = fast->next;
        if(fast == NULL)            //k-1的步长大于链表的长度
            return NULL;
    }
    while(fast->next != NULL)       
    {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}


5.合并两个有序链表:链接

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

请添加图片描述
如示例1,我们需要将两个链表按照从大到小的顺序和成一个新的链表。

思路1:题目中已经分别给出指向两个链表的头结点的指针,我们需要再定义指向新链表头结点的指针NewHead和指向新链表尾结点的指针Ptail,并初始化为NULL。遍历题目给的两个链表,谁存储的值小就先尾插到新链表。

特殊情况:当两个链表其中一个为空时,直接返回非空的链表。
在遍历的过程中,其中一个链表遍历完,而另外一个链表还没有遍历完,那么直接将没有遍历完的链表对应的结点尾插到Ptail,因为在上一条特殊情况的处理下,两个链表都是非空,那么程序就有进入下面代码的while循环,所以Ptail不可能是NULL,不用判断Ptail,直接尾插。
请添加图片描述
代码:

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    if(list1 == NULL)
        return list2;
    if(list2 == NULL)
        return list1;
    struct ListNode* NewHead = NULL,*Ptail = NULL;
    while(list1 != NULL && list2 != NULL)
    {
        if(list1->val > list2->val)
        {
            if(Ptail == NULL)
            {
                NewHead = Ptail = list2;
            }
            else
            {
                Ptail->next = list2;
                Ptail = Ptail->next;
            }
            list2 = list2->next;
        }
        else
        {
            if(Ptail == NULL)
            {
                NewHead = Ptail = list1;
            }
            else
            {
                Ptail->next = list1;
                Ptail = Ptail->next;
            }
            list1 = list1->next;
        }
    }
    if(list1 != NULL)
        Ptail->next = list1;
    if(list2 != NULL)
        Ptail->next = list2;
    return NewHead;
}

思路2:加入哨兵位,那么在进入while循环进行第一次判断时,就不用判断Ptail是否指向空指针了,并且也不用判断两个链表都是非空的,如果有一个链表为空,直接尾插非空链表,因为Ptail指向哨兵位,Ptail不再是空指针。

请添加图片描述
代码:

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    struct ListNode* guard,*Ptail;
    guard = Ptail = (struct ListNode*)malloc(sizeof(struct ListNode));
    if(guard == NULL)
    {
        perror("malloc fail");
        exit(1);
    }
    guard->next = NULL;
    while(list1 != NULL && list2 != NULL)
    {
        if(list1->val > list2->val)
        {
            Ptail->next = list2;
            Ptail = Ptail->next;
            list2 = list2->next;
        }
        else
        {
            Ptail->next = list1;
            Ptail = Ptail->next;
            list1 = list1->next;
        }
    }
    if(list1 != NULL)
        Ptail->next = list1;
    if(list2 != NULL)
        Ptail->next = list2;
    struct ListNode* NewHead = guard->next;
    free(guard);
    return NewHead;
}


6.链表分割:链接

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

如上面的链表中,我们要将小于3的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针,那么我们应该怎么做呢?

思路:定义一个指针,用来遍历题目给的链表。定义两个带哨兵位的新链表,一个尾插旧链表大于x的结点,一个尾插旧链表小于x、等于x的结点,最后,将存储大于x的结点的整个链表插到另外的存储着小于x、等于x的结点的链表的后面。

请添加图片描述
题目只要求小于x的结点排在其余结点之前,上面的图片修改一下,后面代码是正确的。

class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) 
    {
        struct ListNode* greaterHead,*greaterTail = NULL;
        struct ListNode* LessHead,*LessTail = NULL;
        greaterHead = greaterTail = (ListNode*)malloc(sizeof(struct ListNode));
        LessHead = LessTail = (struct ListNode*)malloc(sizeof(struct ListNode*));
        greaterTail->next =LessTail->next = NULL;

        struct ListNode* cur = pHead;
        while(cur != NULL)
        {
            if(cur->val < x)
            {
                LessTail->next = cur;
                LessTail = LessTail->next;
            }
            else
            {
                greaterTail->next = cur;
                greaterTail = greaterTail->next;
            }
            cur = cur->next;
        }
        LessTail->next = greaterHead->next;
        greaterTail->next = NULL;
        struct ListNode* Phead = LessHead->next;
        free(greaterHead);
        free(LessHead);
        return Phead;
    }
};


7.链表的回文结构:链接

题目要求:对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。回文结构的例子:1->2->2->1。给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

思路:找到中间节点(链表个数为奇数)或者第二个中间结点(链表个数为偶数),反转该结点到尾结点的所有结点,将链表原来的头指针和反转后的头指针进行遍历前半段、后半段的链表,观察是不是链表的回文结构,详细看图。

链表结点个数为奇数的回文结构
请添加图片描述

链表结点个数为偶数的回文结构
请添加图片描述
代码:

class PalindromeList {
public:
    struct ListNode* FindMidNode(struct ListNode* Head)
    {
        struct ListNode* slow,*fast;
        slow = fast = Head;
        while(fast != NULL && fast->next != NULL)
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
    struct ListNode* ReverseList(struct ListNode* Head)
    {
        struct ListNode* cur = Head;
        struct ListNode* RHead = NULL;
        while(cur != NULL)
        {
            struct ListNode* Next = cur->next;
            cur->next = RHead;
            RHead = cur;
            cur = Next;
        }
        return RHead;
    }
    bool chkPalindrome(ListNode* A) 
    {
        struct ListNode* mid = FindMidNode(A);
        struct ListNode* Head = ReverseList(mid);
        while(Head->next != NULL)
        {
            if(A->val != Head->val)
                return false;
            
            A = A->next;
            Head = Head->next;
        }
        return true;
    }
};


8.相交链表:链接

题目要求:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回NULL。

请添加图片描述
如图片的情况,两个链表之间存在着相交结点,我们需要判断是否存在这种情况。

思路:先让两个链表找到各自的尾结点,并且在找尾结点的过程,记下两个链表的长度。找到尾结点后,判断两个尾结点是否相同,如果不相同,证明两个链表没有相交,相反就有相交(原因看上图)。在相交的情况下,我们就要找到交点,求出两个链表的长度之差,然后让长度较长的链表的头指针走完这个差值,此时,两个链表的头指针与交点的距离是相等的,让两个头指针一起往后走,直到两者相等,那么该结点就是交点了。

请添加图片描述
代码:

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
    struct ListNode* curA = headA,*curB = headB;
    int lenA = 0,lenB = 0;
    //判断尾结点是否相同
    while(curA->next != NULL)
    {
        curA = curA->next;
        lenA++;
    }
    while(curB->next != NULL)
    {
        curB = curB->next;
        lenB++;
    }
    if(curA != curB)
        return NULL;
    //让长链表的头指针先走差值步数
    int gap = abs(lenA - lenB);
    struct ListNode* longList = headA,*shortList = headB;
    if(lenB > lenA)
    {
        longList = headB;
        shortList = headA;
    }
    while(gap--)
    {
        longList = longList->next;
    }
    //让两个指针一起往后走,相等就是第一个交点
    while(longList != shortList)
    {
        longList = longList->next;
        shortList = shortList->next;
    }
    return longList;
}


9.环形链表:链接

题目要求:给你一个链表的头节点 head ,判断链表中是否有环。
请添加图片描述
如该题目的示例1,我们需要判断该链表是否带环。

思路:利用快慢指针,慢指针每次走一步,快指针每次走两步,所以快指针一直在慢指针的前面,然后快指针会追上慢指针,也就是相遇,此时就可以证明该链表带环。

请添加图片描述
这时就会有人说了,上面的方法是否只适用于示例一的特殊情况?下面,我就来证明该想法的适合于所有的情况。

请添加图片描述
如上面的链表中,我们尝试用快慢指针再次相遇来判断该链表带环。
请添加图片描述

因为N最大也只是接近于环的长度,所以追击小于一圈。

代码:

bool hasCycle(struct ListNode *head) 
{
    struct ListNode* slow,*fast;
    slow = fast = head;
    while(fast != NULL && fast->next != NULL)
    {
        slow = slow->next;
        fast = fast->next->next;

        if(slow == fast)
            return true;
    }
    return false;
}

此时,会不会又有人提出疑问,快指针每次走3步,或者4步、或者5步、或者n步,慢指针不变化,在两个指针距离为N个结点时,快指针能不能追上慢指针。

在快指针每次走4步下
快指针每次走4步,慢指针每次走1步,那么它们的差值就是3
两个指针之间的距离变化
N为偶数
N
N-3
N-6
……
4
1
-2

两个指针指针之间的距离为-2是去什么情况呢?如下图
请添加图片描述
两个指针之间的距离为-2,即为fast指针超过slow指针两个结点,此时,假设环有C个结点,那么fast指针要重新追到slow,就要走C-2个结点,如果C-2依然为偶数,那么两个指针将永远不会相遇。

如果C-2是奇数,如:3、9、12,那么可能相遇,如果是7,那么不可能相遇。即C-2为奇数,两个指针可能相遇。

在上面的情况中,如果fast指针想要追上slow指针,那么要追的圈数大于一圈。

其他情况也是如此,存在着一些追不上的情况,只有快指针每次走两步,慢指针每次走一步,它们之间的差值为1时,才能在所有情况能追上。



10.环形链表 II:链接

**题目要求:**给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回NULL。
请添加图片描述
如示例1,我们需要找到入环的第一个结点,也就是存储着2的结点。

思路:引用上一题的思路,我依然找到快、慢指针的相遇点,然后再定义一个指针从头结点开始,一个指针从相遇点开始,开始向后走,最后将会在入环的第一个结点相遇。

至于为什么一个指针从头结点开始,一个指针从相遇点开始,开始向后走,最后将会在入环的第一个结点相遇,我来说明一下。
请添加图片描述
代码:

struct ListNode *detectCycle(struct ListNode *head) 
{
      struct ListNode* slow,*fast;
      slow = fast = head;
      while(fast != NULL && fast->next != NULL)
      {
          slow = slow->next;
          fast = fast->next->next;

          if(slow == fast)
          {
            struct ListNode* meet = slow;
            struct ListNode* cur = head;
            while(cur != meet)
            {
                cur = cur->next;
                meet = meet->next;
            }
            return cur;
          }
      }
      return NULL;
}


11.复制带随机指针的链表:链接

题目要求:给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节。构造这个链表的深拷贝。深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
请添加图片描述
如示例1,我们需要拷贝该链表,并且保证random的指向与原链表相同。

思路:在每个链表的非空结点后面链接一个新的结点,将该新结点的值改为相应的值,最近将所有链接的结点拆下来,构成一个新的链表,那么此时,链表的拷贝就结束了。

1.链接结点
请添加图片描述
2.设置链接结点的random
请添加图片描述
3.将链接的结点解下来,构成一个新链表
请添加图片描述
代码:

struct Node* copyRandomList(struct Node* head) 
{
    //链接结点
    struct Node* cur = head;
    while(cur != NULL)
    {
        struct Node* Next = cur->next;
        struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
        copy->val = cur->val;
        cur->next = copy;
        copy->next = Next;
        cur = Next;
    }

    //设置链接结点的random
    cur = head;
    while(cur != NULL)
    {
        struct Node* copy = cur->next;
        if(cur->random == NULL)
        {
            copy->random = NULL;
        }
        else
        {
            copy->random = cur->random->next;
        }
        cur = cur->next->next;
    }

    //将结点解下来,链接成新的链表
    struct Node* copyHead = NULL,*copyTail = NULL;
    cur = head;
    while(cur != NULL)
    {
        struct Node* Next = cur->next->next;
        struct Node* copy = cur->next;
        cur->next = Next;
        cur = Next;
        if(copyTail == NULL)
        {
            copyHead = copyTail = copy;
        }
        else
        {
            copyTail->next = copy;
            copyTail = copyTail->next;
        }
    }
    return copyHead;
}


其他链表题目:牛客网 || leetcode

今天,链表的题目讲解就到这里,关注点一点,下期更精彩。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值