链表12道经典OJ题——助你理解链表原理

Posted aaaaaaaWoLan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了链表12道经典OJ题——助你理解链表原理相关的知识,希望对你有一定的参考价值。


注:部分图片从leetcode转载

后面偷懒了,有些图没画

个人认为11题是难度较高的题,如果有大佬觉得不过如此,还望指导指导


1.移除链表元素

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

思路一:设置两个指针遍历链表,cur去查找删除元素,prev保持删除后与后一个节点的联系

需考虑第一个节点就为val的情况,并修改head指向

以示例1为例分析:

第一步:prev为空,cur指向第一个节点,cur指向的1不是要删除的元素,cur,prev均向后走

也就是prev=cur,cur=cur->next

第二步:prev指向1,cur指向2,cur指向的2不是要删除的元素,cur,prev均向后走

第三步:prev指向2,cur指向6,cur指向的6是要删除的元素,free掉6这个节点,cur向后走,prev继续指向2,但节点2的next不再指向6,而是指向3,也就是cur = cur->next,prev->next = cur

第四步:把6删掉,prev指向2,cur指向3,cur指向的3不是要删除的元素,cur,prev均向后走

第五步:prev指向3,cur指向4,cur指向的4不是要删除的元素,cur,prev均向后走

第六步:prev指向4,cur指向5,cur指向的5不是要删除的元素,cur,prev均向后走

第七步:prev指向5,cur指向6,cur指向的6是要删除的元素,free掉6这个节点,cur向后走,prev继续指向5,但节点5的next不再指向6,而是指向NULL,此时cur也指向了NULL,遍历结束

当然,存在首元素就是要删除元素的情况,或者全部都是要删除元素的情况:

此时cur=cur->next,但是prev->next=cur还成立吗?

答案显然是不成立,prev此时还是个空指针,这样显然是非法访问了,所以不能这么写了

删除之后:我们可以假设为将之前的6删掉了,cur还是指向第一个节点,prev当然就还是NULL不用改变,但是此时head就要改变指向了

由此可以直到,所有节点都要删除无非是重复刚才的步骤

代码(只实现函数):

struct ListNode* removeElements(struct ListNode* head, int val){
    struct ListNode* cur = head;
    struct ListNode* prev = NULL;
 
    while (cur != NULL)//遍历链表,cur指向空时遍历结束
    {
        if (cur->val == val)//遇到需删除元素的情况
        {
            if (prev == NULL)//第一个节点就为val的情况
            {
                struct ListNode*new = cur->next;
                free(cur);
                cur = new;
                head = cur;//此时head就不指向第一个节点了
            }
            else//第一个节点不为val的情况
            {
                struct ListNode*new = cur->next;
                free(cur);
                cur = new;
                prev->next = cur;
            }

        }
        else//未遇到需删除元素,prev与cur均向后走
        {
            prev = cur;
            cur = cur->next;

        }
    }

    return head;
}

思路二:设置一个新的哨兵guard结构体,
guard->next=head,prev=guard,这样就不用单独考虑第一个节点就是要删除元素的情况了,而且删除了第一个节点,head也会随之改变,因为prev->next会连接要删除元素的下一个节点,prev->next就是head,head也就改变了,如果第一个节点不是要删除元素,prev->next就不再指向head,head也就不会再改变了,最后记得free(guard)

代码:

struct ListNode {
    int val;
    struct ListNode* next;
    
};
struct ListNode* removeElements(struct ListNode* head, int val) {
    struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));
    guard->next = head;
    struct ListNode* prev = guard;
    struct ListNode* cur = head;

    while (cur != NULL)
    {
        if (cur->val == val)
        {
            struct ListNode* new = cur->next;
            free(cur);
            prev->next = new;
            cur = new;
        }
        else
        {
            prev = cur;
            cur = cur->next;
        }
    }

    head = guard->next;
    free(guard);
    guard = NULL;

    return head;
}

2.反转链表

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

思路一:创建三个指针n1,n2,n3

初始化n1为空,n2指向1,使1指向NULL也就是n1,2指向1也就是n2,再依次将n1,n2,n3向后走,如此循环往复

以示例1为例分析:

第一步:n2->next = n1,使1指向空,n1 = n2, n1再指向1,n2 = n3, n2再指向2,n3 = n3->next,n3再指向3

第二步:n2->next = n1,使2指向1,n1 = n2, n1再指向2,n2 = n3, n2再指向3,n3 = n3->next,n3再指向4

第三步:n2->next = n1,使3指向2,n1 = n2, n1再指向3,n2 = n3, n2再指向4,n3 = n3->next,n3再指向5

第四步:n2->next = n1,使4指向3,n1 = n2, n1再指向4,n2 = n3, n2再指向5,n3 = n3->next,n3就指向NULL了

这里注意,下一次就不能再使n3 = n3->next了,否则就是非法访问了,所以要加个判断条件if (n3 != NULL),才能执行

n3 = n3->next

第五步:n2->next = n1,使5指向4,n1 = n2, n1再指向5,n2 = n3, n2再指向NULL,n3已经是NULL了,就不能再向后走了

最后的结果就是:此时遍历结束,完成反转

代码(只实现函数):

struct ListNode {
    int val;
    struct ListNode* next;
    
};

struct ListNode* reverseList(struct ListNode* head) {

    if (head == NULL || head->next == NULL)
    {
        return head;
    }

    struct ListNode* n1 = NULL;
    struct ListNode* n2 = head;
    struct ListNode* n3 = head->next;



    while (n2 != NULL)
    {
        //翻转
        n2->next = n1;
        
        //迭代
        n1 = n2;
        n2 = n3;
        if (n3 != NULL)
        {
            n3 = n3->next;
        }
    }

    return n1;
}

思路二:头插(不创建新节点)

创建cur,newhead两个指针用头插的方式完成反转

初始化cur为head,newhead为NULL,即假设从一个空链表(说是新链表,但实际没有开辟空间)开始,向前依次插入1,2,3,4,5

以示例1为例分析(图中简称newhead为new):

第一步:一开始为空链表,向空链表头插1,头部就变成了1,new此时指向1,1也要重新指向NULL,创建临时指针tmp保存1指向的2,cur向后指向2,注意这里不能free(cur),因为1这个空间只开辟了一份,如果free了就不存在了,头插就类似把原节点转移位置

第二步:…

第三步:…

如此往复,就可以把所有1,2,3,4,5均头插至新链表,最后的头部也就是5,newhead指向的就是5

(别问为什么没图,问就是太难画)

代码(只实现函数):

 struct ListNode {
    int val;
    struct ListNode* next;
    
};

 struct ListNode* reverseList(struct ListNode* head) {
     struct ListNode* cur = head;
     struct ListNode* newhead = NULL;

     while (cur != NULL)
     {
         struct ListNode* tmp = cur->next;
         cur->next = newhead;
         newhead = cur;

         //不能free

         cur = tmp;
     }

     return newhead;
 }

3.链表的中间结点

OJ链接
给定一个头结点为 head 的非空单链表,返回链表的中间结点。

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

思路:利用快慢指针

慢指针走一步,快指针走两步,当快指针指向NULL时(偶数个数字的情况)或快指针的next指向NULL时(奇数个数字的情况)

慢指针指向的位置就是所求节点

以示例1和2为例分析:

示例1:

第一步:slow和quick一开始均指向1,然后slow走一步,quick走两步

第二步:slow此时指向2,quick此时指向3,然后slow走一步,quick走两步

第三步:quick的next已经指向空了,slow此时就是要找的位置

示例2:

第一步:slow和quick一开始均指向1,然后slow走一步,quick走两步

第二步:slow此时指向2,quick此时指向3,然后slow走一步,quick走两步

第三步:slow此时指向3,quick此时指向5,然后slow走一步,quick走两步

第四步:quick此时已经指向NULL了,slow就是要查找的位置

代码(只实现函数):

 struct ListNode {
    int val;
    struct ListNode* next;
    
};


struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* quick = head;
    struct ListNode* slow = head;

    while (quick != NULL && quick->next != NULL)
    {
        slow = slow->next;
        quick = quick->next->next;
    }

    return slow;
}

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

OJ链接

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

注:一般题目说输入一个链表,没有特别说明,就是指不带头节点的单链表

思路一:遍历链表,计算链表长度,再遍历一次,找到(链表长度-k)个节点

该方法较简单,不作详细解释

思路二:快慢指针,快指针先走k步,慢指针再走,快指针指向空时,慢指针指向节点即为所求

考虑极端情况:1.k>链表长度; 2.链表为空

以示例1为例分析:

第一步:一开始slow 和 quick都指向1,quick向后先k走,也就是1步

第二步:slow和quick一起向后走,直到quick指向空

第三步:quick已经指向空,slow所指的节点5就是所求节点

两种极端情况的解决方法:

1.k>链表长度

k>链表长度,找倒数第k个节点说明找到的是空,所以返回空

2.链表为空,直接返回空

代码(只实现函数部分):

 struct ListNode {
 	int val;
 	struct ListNode *next;
  };
 

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k) {
    // write code here
    struct ListNode* quick = pListHead;
    struct ListNode* slow = pListHead;

    if (pListHead == NULL)
        return NULL;

    while ((k--) != 0)//快指针先走
    {
        if (quick == NULL)//处理k>链表长度的情况
        {
            return NULL;
        }

        quick = quick->next;
    }

    while (quick != NULL)//快慢同时出发
    {
        slow = slow->next;
        quick = quick->next;
    }

    return slow;
}

5.合并两个有序链表

OJ链接

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

思路:类似于合并两个有序数组,创建两个指针分别指向两个链表,将两个指针指向的val分别进行比较,取小的尾插到另一个大的节点后面,并且创建一个尾指针来维护新链表的末尾

考虑链表为空的情况:可能链表一为空链表二不为空,也可能链表二为空链表一不为空,也可能两者都为空

以示例1为例分析:

第一步:先取一个较小的节点做新链表的第一个节点,方便后面尾插(前提是两指针不为空)

假设当cur2指向的元素大于cur1指向的元素时,将cur1所指元素尾插至新链表后,否则将cur2所指元素尾插至新链表后

cur1指向红色链表,cur2指向紫色链表,此时cur1指向的1等于cur2指向的1,所以将cur2指向的1作为头部将cur1指向的1尾插至cur2指向的1之后

同时让新链表的头部phead指向紫色链表的节点1,方便之后返回结果

分离得到:

要设置指针保存指向紫色链表3的位置,才能再尾插,也就是使 cur2 = cur2->next

我们发现实际上我们并没有创建新链表,而是将其中一个作为新链表

第二步:比较cur1指向1与cur2指向的3,1<3,所以将节点1尾插至新链表的后面,并且使cu1 = cur1->next,tail此时也指向红色节点1

得到结果:

第三步:比较cur1指向2与cur2指向的3,2<3,所以将节点2尾插至新链表的后面,并且使cu1 = cur1->next,tail此时也指向红色节点2

得到结果:

第四步:比较cur1指向4与cur2指向的3,3<4,所以将节点3尾插至新链表的后面,并且使cu2 = cur2->next,tail此时也指向紫色节点3

得到结果:

第五步:比较cur1指向4与cur2指向的4,cur2指向的4不大于cur1指向的4,所以将节点4尾插至新链表的后面,并且使cu2 = cur2->next,tail此时也指向紫色节点4

得到结果:

第六步:cur2已经指向空了,再将另一个链表剩下的节点插至新链表后即可,注意,这里只需要连接一次,因为圣血的节点本就是相连的

代码(只实现函数):

上述分析中的cur1,cur2在代码中是l1,l2

 struct ListNode {
    int val;
    struct ListNode* next;
    
};

struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2) {

    //判断是否为空
    if (l1 == NULL)
    {
        return l2;
    }
    if (l2 == NULL)
    {
        return l1;
    }

    struct ListNode* phead = NULL;
    struct ListNode* tail = NULL;

    if (l2->val > l1->val)//先取小节点作新链表的第一个节点
    {
        phead = tail = l1;
        l1 = l1->next;
    }
    else
    {
        phead = tail = l2;
        l2 = l2->next;
    }

    while (l1 != NULL && l2 != NULL)
    {
        if (l2->val > l1->val)
        {
            tail->next = l1;
            tail = tail->next;
            l1 = l1->next;
        }
        else
        {
            tail->next = l2;
            tail = tail->next;
            l2 = l2->next;
        }

    }

    //拷贝剩下的节点
    if (l2 == NULL)
    {
        tail->next = l1;
    }
    else
    {

        tail->next = l2;
    }

    return phead;
}

还有一种哨兵解法,就是创建一个新节点,同样也是取小的尾插至新节点之后,省去了判断两个链表是否为空的情况,但返回前需要free掉该空间

初始时:

后面与上述方法一致

代码:

     struct ListNode {
        int val;
        struct ListNode* next;
    
    };
     struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2) {
        //创建哨兵
        struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));
        //维护新空间的尾部
        struct ListNode* tail = guard;


        while (l1 != NULL && l2 != NULL)
        {
            if (l2->val > l1->val)
            {
                tail->next = l1;
                tail = tail->next;
                l1 = l1->next;
            }
            else
            {
                tail->next = l2;
                tail = tail->next;
                l2 = l2->next;
            }
        }

        //拷贝剩下的节点
        if (l2 == NULL)
        {
            tail->next = l1;
        }
        else
        {

            tail->next = l2;
        }

        //释放哨兵空间
        struct ListNode* tmp = guard->next;//记得是存放guard->next
        free(guard);
        guard = NULL;

        return tmp;
    }

6.链表分割

OJ链接

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

思路:创建两个新链表,一个存放比x小的节点,另一个存放等于或大于x的节点,创建cur遍历原链表,填充好新链表后,将两个链表连接,再返回

注意一点:将两个链表链接完成后,将存放较大数字的链表尾部的next置空,否则有可能该next还指向其他节点,导致连接后的链表形成环

比如:

此时9的next还连着3,就会形成一个环

代码(只实现函数部分):

ListNode* partition(ListNode* pHead, int x) {
        // write code here
        ListNode* lesshead = (ListNode*)malloc(sizeof(ListNode));
        ListNode* lesstail = lesshead;
        lesshead->next = NULL;
        ListNode* greaterhead = (ListNode*)malloc(sizeof(ListNode));
        ListNode* greatertail = greaterhead;
        greaterhead->next = NULL;
        ListNode* cur = pHead;

        while (cur != NULL)
        {
            if (cur->val < x)
            {
                lesstail->next = cur;
                lesstail = cur;
                cur = cur->next;
            }
            else
            {
                greatertail->next = cur;
                greatertail = cur;
                cur = cur->next;
            }
        }
        
        lesstail->next = greaterhead->next;
        greatertail->next = NULL;
        pHead = lesshead->next;
        free(lesshead);
        free(greaterhead);
        
        return pHead;
    }

7.链表的回文结构

OJ链接

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

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

bool是c++的变量类型,只有true和false两种

思路一:创建一个数组int a[900],将链表的数据按顺序放入数组

这样可以,但是空间复杂度不符合O(1),就不多解释了

思路二:1.先找到中间节点; 2.再反转中间节点及之后的节点;3.从头节点处和中间节点处开始比较

以示例1为例:

再分析一个奇数位的链表:

到这里可能有人奇怪,3也要反转吗?

我们先来看代码,由于找中间节点与反转链表我们之前实现过,这里就不再写代码怎么实现了

typedef struct ListNode {
    int val;
    struct ListNode* next;
}ListNode;


struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* quick = head;
    struct ListNode* slow = head;

    while (quick != NULL && quick->next != NULL)
    {
        slow = slow->next;
        quick = quick->next->next;
    }

    return slow;//返回中间节点
}

//反转链表
struct ListNode* reverseList(struct ListNode* head) {

    if (head == NULL || head->next == NULL)
    {
        return head;
    }

    struct ListNode* n1 = NULL;
    struct <

以上是关于链表12道经典OJ题——助你理解链表原理的主要内容,如果未能解决你的问题,请参考以下文章

1024狂欢力扣经典链表OJ题合集

八道经典的面试链表题--------快乐人的java巩固日记[1]

[ 链表OJ题--C语言 ] 合并两个有序链表

[ 链表OJ题--C语言] 相交链表 两个链表的第一个公共结点

[ 链表OJ题 --C语言实现 ] 反转链表

看一遍就理解,图解单链表反转