【手撕数据结构】链表高频面试题

移除链表元素

在这里插入图片描述

思路:由题目可知我们需要在给定的一个链表中移除值为val的节点,这里需要注意的情况就是全是val的链表移除后为空链表和传过来的链表是空链表的情况,如果直接对不是val的节点进行连接,返回头结点会无法处理头结点也是val的情况,这里不妨我们用两个指针开始都设置NULL,用一个指针指向头结点开始判断,必须是从头结点开始遍历不等于val的节点才能连接,不然就不能连接。这时候就处理了两种情况

typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
    ListNode* NewHead = NULL;   //一开始就为空处理全是相同元素的情况和传过来是空链表的情况
    ListNode* NewTail = NULL;
    ListNode* pcur = head;
    while(pcur)
    {
        if(pcur->val != val)
        {
            if(NewHead == NULL)
            {
                NewHead = NewTail = pcur;
            }
            else
            {
                NewTail->next = pcur;
                NewTail = NewTail->next;
            }
           
        }
         pcur = pcur->next;
    }
    if(NewTail != NULL) //防止堆空链表和移除后是空链表的空指针解引用
    {
        NewTail->next = NULL;
    }
    return NewHead;
}

注意最后一个节点连接的时候,万一他的下一个节点还是val值,必须把他的next指针设置NULL;

反转链表

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

在这里插入图片描述
思路:由题目可知,我们需要把给定的链表所以节点反转,并返回新的头结点。这里要注意链表的最后一个节点是一个NULL,所以我们反转的链表的尾节点也是一个NULL。反转无非就是改变next指针的指向即以下

  1. 定义三个指针n1,n2,n3.n1指向NULL(新尾节点),n2指向原头结点,n3指向第二个节点
  2. 把n2头节点的next指针指向n1,然后n1移到n2,n2移到n3,以此类推改变节点的next指针方向
  3. 直到n2指向NULL,循环结束,返回n1

注意:为什么定义三个指针:定义两个指针不行吗?不行,因为我们要先改变节点next指针的方向,但是如果直接改变了这个节点的next指针我们如何找到下一个节点呢?所以定义三个指针

n1:记录指针指向将要反转的结点反转后要指向的位置。
n2:记录指针指向将要反转的结点。
n3:记录指针指向将要反转的结点的下一个结点。

在这里插入图片描述

注意,这时这3个指针统一后移时,n3指针的后移将失败,因为n3后移前指向的是NULL,我们不能执行以下这句代码:

n3 = n3->next;

所以我们后移n3指针前需判断其是否为空。

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

链表的中间节点

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

如果有两个中间结点,则返回第二个中间结点。(给定的链表是非空链表)

思路:快慢指针法,定义两个指针,两个都指向头节点,慢指针每次走一步,快指针每次走两步,最终返回的中间节点就是慢指针

注意:当fast指向空或者fast指针的next指针指向的节点为空的时候,就停止遍历返回慢指针

在这里插入图片描述
在这里插入图片描述
偶数个有两个中间节点(如上面3,4),通常我们取的是第二个中间节点,也是为什么fast快指针也从头结点开始遍历。这样不用单独处理slow指向第一个中间节点的情况

 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;
}

倒数第k个节点

实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。

反转链表(初阶)

在这里插入图片描述
思路:倒数第二个节点,是不是就是反转链表后的第二个节点?所以我们结合前面的反转链表的三指针法,然后堆反转链表变量k次就可以找的倒数的第k个节点

typedef struct ListNode ListNode;
int kthToLast(struct ListNode* head, int k){
    ListNode* n1 = NULL;
    ListNode* n2 = head;
    ListNode* n3 = n2->next;
    while(n2)
    {
        n2->next = n1;
        n1 =n2;
        n2 = n3;
        if(n3)
        {
            n3 = n3->next;
        }
    }
    while(--k)
    {
        n1 = n1->next;
    }
    return n1->val;

}

快慢指针法(进阶)

如果改一下题目,不能改变原链表的结构并且k不一定有效(有可能大于总结点数)该如何做呢?这样反转链表就不适用了吧。这时候就又可以用我们的快慢指针了

思路:这里的"快慢指针"不是一个指针走得快,一个指针走的慢。这里我们是为了找倒数第k个节点。我们每个链表的最后一个节点都是NULL,那么从倒数第k个节点开始走k步是不是就是null呢?所以我们不妨先让快指针fast走k步,然后慢指针和快指针再同时向后移动,直到fast快指针指向null时slow慢指针就是倒数第k个节点

注意:如果fast在走k步时就提前遇到NULL,那么说明k大于总节点数。这时不存在倒数第k个节点。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

合并两个有序链表

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

在这里插入图片描述
在这里插入图片描述

思路:创建一个新链表,对两个链表进行比较,那个链表的节点数据小,就插入到新链表中,直到其中两个链表其中一个为空停止(不存在同时为空的情况,数据一致也会有一个数据还没插入)。然后判断两个链表谁还没有为空,将不为空的链表尾插到新链表中。

注意:

  • 两个链表为空直接返回NULL
  • 其中一个链表为空返回不为空的链表
    在这里插入图片描述
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 = NULL, *NewTail = NULL;
    ListNode* l1 = list1, *l2 = list2;
    while(l1 && l2)
    {
        if(l1 -> val < l2->val)
        {
            if(NewHead == NULL)
            {
                NewHead = NewTail = l1;
            }
            else
            {
                NewTail->next = l1;
                NewTail = NewTail->next;
            }
            l1 = l1->next;
        }
        else
        {
            if(NewHead == NULL)
            {
                NewHead = NewTail = l2;
            }
            else
            {
                NewTail->next = l2;
                NewTail = NewTail->next;
            }
             l2 = l2->next;
        }
    }
    if(l1)
    {
        NewTail->next = l1;
    }
    else
    {
        NewTail->next = l2;
    }
    return NewHead;
}

上面的链表结构我们是用一个空链表来存储,会导致插入头结点的时候总是会判断是不是NULL,导致代码冗余,这里我们不再使用空链表,使用带头的链表来存储,这样就直接可以在带头链表的下一个节点的位置插入。(为什么不在这个带头节点直接插入,这是动态开辟的空间,后面需要释放)

在这里插入图片描述

在这里插入图片描述

 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,*NewTail;
    NewHead = NewTail =(ListNode*)malloc(sizeof(ListNode));
    ListNode* l1 = list1, *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;
    }
    else
    {
        NewTail->next = l2;
    }
    ListNode* ret = NewHead->next;
    free(NewHead);
    NewHead = NULL;
    return ret;
}

链表分割

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

思路:由题目可知,我们需要把小于x的节点排在其他指点之前,不能改变原来的数据顺序
不改变数据顺序:
在这里插入图片描述

所以这里不是让我们排升序

我们不妨创建两个链表,一个链表存储小于x的数据,一个链表存吃啥大于等于x的数据,然后把两个链表的首尾相连即可

1.把小于x的结点尾插到less链表,把大于x的结点尾插到greater链表
在这里插入图片描述
2.将less链表与greater链表链接起来。
在这里插入图片描述
注意:
1.链接后的链表的最后一个结点的指针域需要置空,否则可能造成链表成环。
2.返回的头指针应是lessHead->next,而不是lessHead。因为这是头节点

这是带头版本的做法

#include <cstddef>

class Partition {

public:

    ListNode* partition(ListNode* pHead, int x) {

        // write code here

        //创建两个非空链表,小链表(存储小于定值x)和大链表(存储大于等于定值x)

        ListNode* LessNewhead,*LessNewtail;

        ListNode* GreaterNewhead,*GreaterNewtail;

        LessNewhead=LessNewtail=(ListNode*)malloc(sizeof(ListNode));

        GreaterNewhead=GreaterNewtail=(ListNode*)malloc(sizeof(ListNode));

        if(LessNewhead==NULL && GreaterNewhead==NULL)

        {

            perror("malloc failed");

            exit(1);

        }

    ListNode* pcur=pHead;

    while(pcur)

    {

        if(pcur->val<x)

        {

            LessNewtail->next=pcur;

            LessNewtail=LessNewtail->next;

        }

        else

        {

            GreaterNewtail->next=pcur;

            GreaterNewtail=GreaterNewtail->next;

        }

        pcur=pcur->next;

    }

    //将小链表的尾连接到大链表的头

    LessNewtail->next=GreaterNewhead->next; //这里nGext是因为大链表的头还是指向第一个无效空间

    GreaterNewtail->next=NULL;      //一定要把大链表的下一个节点设置NULL,因为大链表村上的是大于等于x的节点,若其中一个节点的next指针指向小链表节点,则会死循环形成环,如 5 1 3 6 2,其中小链表:1 2,大链表:5 3 6,因为6的下一个节点是2,下一次循环又会指向小链表的节点2,导致 2 5 3 6,2 5 3 6....死循环

    ListNode* ret=LessNewhead->next;

    free(LessNewhead);

    free(GreaterNewhead);

    LessNewhead=NULL;

    GreaterNewhead=NULL;

    return ret;

    }

但是不带头版本又多了一个判断

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
#include <cmath>
#include <functional>
#include <sys/ucontext.h>
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        // write code here
       ListNode* LessHead = NULL, *LessTail = NULL;
       ListNode* GreaterHead = NULL, *GreaterTail = NULL;
       ListNode* pcur = pHead;
        while (pcur) {
            if(pcur->val < x)
            {
                if(LessHead == NULL)
                {
                    LessHead = LessTail = pcur;
                }
                else 
                {
                    LessTail->next = pcur;
                    LessTail = LessTail->next;
                }
            }
            else 
            {
                if(GreaterHead == NULL)
                {
                    GreaterHead = GreaterTail =pcur;
                }
                else 
                {
                   GreaterTail->next = pcur;
                   GreaterTail = GreaterTail->next;
                }
            }
            pcur = pcur->next;
        }
         if (GreaterTail) {
            GreaterTail->next = nullptr;
        }
        if (LessTail) {
            LessTail->next = GreaterHead;
            return LessHead;
        } else {
            return GreaterHead;
        }
    }
  

};

在这里插入图片描述
为什么要判断连接的链表为空呢,因为题目也没说给的数据全部比x大或者全部比x小呀
例如:4 4 4 4 ,x=3
如果直接对LessTail解引用此时Less这个链表为空,就是对空指针解引用
在这里插入图片描述
有的人就要问了,那为啥带头的不判断,带头本身就有一个头结点不存在链表为null的情况,也就不会对null解引用
在这里插入图片描述

这里还是推荐带头的写法,会清除对头结点插入判断的代码冗余

链表的回文结构

在这里插入图片描述
我们需要找到传入链表的中间结点,并将中间结点及其后面结点进行反转,然后再将原链表的前半部分与反转后的后半部分进行比较,若相同,则该链表是回文结构,否则,不是回文结构。
1.找中间节点并返回
在这里插入图片描述
2.对中间节点及以后得节点进行反转
在这里插入图片描述
3.比较链表的前半部分与后半部分的结点值,若相同则是回文结构,否则,不是回文结构。
在这里插入图片描述
注意:就算传入的链表是结点数为奇数的回文结构,该思路也可以成功判断。
例如,以下链表反转其后半部分后,我们看似链表应该是这样的。
在这里插入图片描述
但反转后的链表并不是这样的,而应该是下面这样:

因为我们反转的是中间结点及其后面的结点,并没有对前面的结点进行任何操作,所以结点5所指向的结点应该还是结点7。
在这里插入图片描述

于是该链表的比较过程应该是这样的:1等于1,3等于3,5等于5,7等于7,然后RHead指针指向NULL。所以判断该链表是回文结构。

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
#include <list>
class PalindromeList {
public:
    ListNode* FIndMiddle(ListNode* phead)
    {
        ListNode* slow = phead;
        ListNode* fast = phead;
        while(fast && fast->next)
        {
            slow = slow->next;
            fast = fast->next;
        }
        return slow;
    }
    ListNode* ReverseList(ListNode* phead)
    {
        ListNode* n1 = NULL;
        ListNode* n2 = phead;
        ListNode* n3 = n2->next;
        while(n2)
        {
            n2->next = n1;
            n1 = n2;
            n2 = n3;
            if(n3)
            {
                n3 = n3->next;
            }
        }
        return n1;
    }
    bool chkPalindrome(ListNode* A) {
        // write code here
        //找到中间节点
        ListNode* mid = FIndMiddle(A);
        //将中间节点之后反转
        ListNode* right = ReverseList(mid);
        ListNode* pcur = A;
        while(right)
        {
            if(right->val != pcur->val)
            {
                return false;
            }
            right = right->next;
            pcur = pcur->next;
        }
        return true;
    }
};
  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值