【数据结构】链表相关OJ题 (万字详解)(1)

简介: 【数据结构】链表相关OJ题 (万字详解)(1)

一、移除链表元素

题目链接

203. 移除链表元素 - 力扣(LeetCode)

题目描述

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

思路1

遍历链表,比对每一个节点的数据与val是否相等,如果相等,就free掉该节点。时间复杂度:O(N) 空间复杂度:O(1)

易错点

1、当链表的头结点的数据等于val时,我们free掉该节点后需要挪动head指针,让其指向新的头结点;

2、我们在遍历链表的时候需要记录前一个节点的地址,因为当我们free掉当前节点之后,我们要让前一个节点的next;链接到当前节点的下一个节点;

代码实现

//法一:遍历链表,找到等于val的节点就free掉
struct ListNode* removeElements(struct ListNode* head, int val){
    struct ListNode* cur = head, *prev = NULL;
    while(cur)
    {
        if(cur->val == val)  //移除
        {
            if(cur == head)  //头删,需要改变头结点
            {
                head = cur->next;
                free(cur);
                cur = head;
            }
            else  //非头删,正常移除
            {
                prev->next = cur->next;
                free(cur);
                cur = prev->next;
            }
        }
        else  //保留
        {
            prev = cur;
            cur = cur->next;
        }
    }
    return head;
}

2020062310470442.png

思路2

遍历链表,将不等于val的节点尾插到一个新的链表,将等于val的节点free掉。时间复杂度:O(N) 空间复杂度:O(1)

易错点

1、由于我们是把原链表中的节点尾插到新链表中去,所以我们插入元素的时候需要判断链表是否为空,如果为空,我们需要改变新链表的头结点;


2、当然,我们也可以把我们的新链表设计为带哨兵位的,这样我们直接进行尾插就行,但是要注意我们返回的应该是guard->next,因为哨兵位头结点不用于存储数据,同时在return之前记得把哨兵位头结点释放掉;


3、由于原链表中最后一个节点的数据可能等于val,所以我们需要将新链表中尾结点的next置为NULL,防止通过它来访问已经被释放掉的节点。

代码实现

//法二:取不等于val的节点尾插
struct ListNode* removeElements(struct ListNode* head, int val){
    struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));  //哨兵位头结点
    guard->next = NULL;  //当原链表为空时我们可以正常返回
    struct ListNode* tail = guard, *cur = head;
    while(cur)
    {
        if(cur->val == val)  //移除
        {
            struct ListNode* next = cur->next;
            free(cur);
            cur = next;
        }
        else  //尾插
        {
            tail->next = cur;
            tail = cur;
            cur = cur->next;
        }
    }
    tail->next = NULL;  //避免当最后一个元素别移除时前面还保留它的地址,造成野指针
    head = guard->next;
    free(guard);
    guard = NULL;
    return head;
}

2020062310470442.png

二、反转链表

题目链接

206. 反转链表 - 力扣(LeetCode)

题目描述

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

思路分析

思路1

反转每一个节点的指向:定义三个节点指针prev cur next,最开始让prev指向NULL,cur指向head,next指向cur->next,然后让cur->next指向prev,最后进行迭代,即让prev=cur、cur=next、next=cur->next,直到cur为空时循环结束,此时prev为反转后的链表的头结点。时间复杂度:O(N) 空间复杂度:O(1)

2020062310470442.png

易错点

1、我们需要定义一个next变量来保存下一个节点的地址,因为当我们让当前节点指向前一个节点时,我们会丢失下一个节点的地址;

2、当cur->next为NULL时,我们再将cur->next=prev,然后cur=next,此时所有节点反转完毕,cur==NULL,循环结束;

代码实现

//法一:让每一个节点指向它的前一个节点(三指针)
struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* prev = NULL, *cur = head, *next = NULL;
    while(cur)
    {
        //翻转节点的指向
        next = cur->next;
        cur->next = prev;
        //迭代
        prev = cur;
        cur = next;
    }
    return prev;  //此时尾结点是新的头节点
}

2020062310470442.png

思路2

将原链表中的节点头插到新链表中,然后返回新链表的头。时间复杂度:O(N) 空间复杂度:O(1)

易错点

如果我们的链表不带头,我们每头插一个元素都需要改变头结点。

代码实现

//法二:取出链表的每一个节点头插
struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* newhead = NULL;
    struct ListNode* cur = head;
    while(cur)
    {
        struct ListNode* next = cur->next;
        cur->next = newhead;
        newhead = cur;
        cur = next;
    }
    return newhead;
}

2020062310470442.png

三、链表的中间节点

题目链接

876. 链表的中间结点 - 力扣(LeetCode)

题目描述

给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。时间复杂度:O(N) 空间复杂度:O(1)

思路分析

思路1

遍历两遍数组,第一遍求出链表长度,第二步找出链表的中间节点并返回。

代码实现

//法一:遍历两次链表,第一次找出链表有几个节点,第二次返回链表的中间节点
struct ListNode* middleNode(struct ListNode* head){
    struct ListNode* cur = head;
    int count = 0;
    while(cur)
    {
        count++;
        cur = cur->next;
    }
    cur = head;
    count /= 2;
    while(count--)
    {
        cur = cur->next;
    }
    return cur;
}

2020062310470442.png

思路2

由于这道题用第一种方法实现十分简单,所以在面试中面试官会加一个限制条件:要求只能遍历一遍链表;这时候就只能用快慢指针来解题了;


快慢指针:定义两个指针 – fast slow,慢指针一次走一步,快指针一次走两步;当链表长度为奇数,fast->next == NULL时,slow 为中间节点;当链表长度为偶数,fast == NULL 时,slow 为中间节点。时间复杂度:O(N) 空间复杂度:O(1)

2020062310470442.png

易错点

我们在写while循环的条件时,必须写成 fast && fast->next,不能写成 fast->next && fast,因为当链表长度为偶数时,后面这种写法会发生空指针的解引用。

代码实现

//法二:使用快慢指针,slow一次走一步,fast一次走两步,只遍历一遍数组
//奇数个节点时,当fast->next == NULL时,slow刚好到达中间节点
//偶数个节点时,当fast == NULL时,slow刚好达到中间节点
struct ListNode* middleNode(struct ListNode* head){
    struct ListNode* fast, *slow;
    slow = fast = head;
    //注意:while条件中fast一定要写前面,不然偶数个时fast->next会造成空指针解引用
    while(fast && fast->next)  //节点是奇数还是偶数未知注意:
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

2020062310470442.png

四、链表中倒数第K个节点

题目链接

链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)

题目描述

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

思路分析

思路1也是遍历两遍链表,和上面题一样,这里我直接阐述思路2,思路2也是利用快慢指针的思想;O(N) 空间复杂度:O(1)


快慢指针:定义两个指针 – fast slow,先让 fast 走K/K-1步,然后让 fast 和 slow 一起走; fast 先走K步时,当 fast == NULL,slow 为倒数第K个元素;fast 先走K-1步时,当 fast->next == NULL,slow 为倒数第K个元素;这里与链表长度为奇数或者偶数无关。

2020062310470442.png

易错点

注意:在做OJ类题目时,我们要考虑测试用例的边界情况,比如K等于1,K等于链表长度;还需要考虑测试用例的极端情况,比如链表为空,K为0,K大于链表长度;

在这道题中,我们正常编写的代码一般来说对于边界情况是能够正常通过的,对于极端情况中的NULL,K等于0也是能够通过的,但是K大于链表长度的情况很可能会被我们忽略;

当K大于链表长度时,如果我们 在K-- 的 while 循环中没有对 fast进 行空指针检查的话,那么 fast 会不断往后走,直到 fast == NULL 时仍然不会停下,这时候就会造成对空指针解引用的问题。

2020062310470442.png

代码实现

//法二:快慢指针:让fast先走K/k-1步,然后slow和fast同时走,这时fast和slow之间就相差K/k-1个节点
//当fast先走K步时:fast == NULL
//当fast先走K-1步时:fast->next == NULL
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    // write code here
    struct ListNode* fast, *slow;
    fast = slow = pListHead;
    while(k--)  //让fast先走K步
    {
        if(fast == NULL)
        {
            return NULL;
        }
        fast = fast->next;
    }
    while(fast)
    {
        fast = fast->next;
        slow = slow->next;
    }
    return slow;
}

2020062310470442.png

五、合并两个有序链表

题目链接

21. 合并两个有序链表 - 力扣(LeetCode)

题目描述

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

思路分析

这道题的解题思路和顺序表中的合并两个有序数组的思路一模一样,只是这里尾插的是节点而已。时间复杂度:O(N) 空间复杂度:O(1)

易错点

1、由于我们是把原链表中的节点尾插到新链表中去,所以我们插入元素的时候需要判断链表是否为空,如果为空,我们需要改变新链表的头结点;


2、当然,我们也可以把我们的新链表设计为带哨兵位头的,这样我们直接进行尾插就行,但是要注意我们返回的应该是guard->next,因为哨兵位头结点不用于存储数据,同时在return之前我们要记得把哨兵位头结点释放掉;

代码实现

//取小的节点尾插
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));  //哨兵位
    guard->next = NULL;
    struct ListNode* tail = guard, *cur1 = list1, *cur2 = list2;
    while(cur1 && cur2)  //取小的尾插
    {
        if(cur1->val < cur2->val)  
        {
            struct ListNode* next = cur1->next;
            tail->next = cur1;
            tail = cur1;  //更新尾
            cur1 = next;
        }
        else
        {
            struct ListNode* next = cur2->next;
            tail->next = cur2;
            tail = cur2;
            cur2 = next;
        }
    }
    //链接剩余的节点
    if(cur1)
        tail->next = cur1;
    if(cur2)
        tail->next = cur2;
    struct ListNode* head = guard->next;
    free(guard);
    guard = NULL;
    return head;
}

2020062310470442.png

六、分隔链表

题目链接

面试题 02.04. 分割链表 - 力扣(LeetCode)

86. 分隔链表 - 力扣(LeetCode)

题目描述

分隔链表分为初阶版和进阶版,初阶版不要求我们保留每个分区中各节点的初始相对位置,而进阶版则要求要求我们保留每个分区中各节点的初始相对位置,这里我们讲解进阶版。

2020062310470442.png

20200623104134875.png

思路分析

思路1

取原链表中val小于x的节点节点头插,val大于x的节点尾插;此方法会改变链表中节点的初识相对位置,只适用于初阶。时间复杂度:O(N) 空间复杂度:O(1)

思路2

将原链表中val小于x的节点尾插到一个新链表中,将val大于x的节点尾插到另一个新链表中,最后将两个新链表链接起来。时间复杂度:O(N) 空间复杂度:O(1)

易错点

1、我们可以将两个新链表设计为带头链表,这样可以免去插入第一个元素时的判断步骤,避免犯错;

2、我们需要将用于链接val大于x的链表的尾结点的next置空,避免最后一个节点的val小于x时前一个节点的next仍指向它,从而形成环。

代码实现

//把大于等于X的节点和小于X的节点分别尾插到新的链表中,然后再把两个链表链接起来
struct ListNode* partition(struct ListNode* head, int x){
    //开辟两个链表的头结点
    struct ListNode* lessGuard = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* greaterGuard = (struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* lessTail = lessGuard, *greaterTail = greaterGuard, *cur = head;
    lessGuard->next = NULL;
    greaterGuard->next = NULL;
    while(cur)
    {
        if(cur->val < x)  //如果小于X就尾插到less中
        {
            lessTail->next = cur;
            lessTail = cur;
        }
        else  //否则就尾插到greater中
        {
            greaterTail->next = cur;
            greaterTail = cur;
        }
        cur = cur->next;
    }
    //让less的尾结点链接到greater的头结点
    lessTail->next = greaterGuard->next;
    //把greater的头结点置空,防止当最后一个元素小于X被尾插到less时仍然指向它,从而形成环,造成死循环
    greaterTail->next = NULL;
    head = lessGuard->next;
    //释放哨兵位头结点
    free(greaterGuard);
    free(lessGuard);
    greaterGuard = NULL;
    lessGuard = NULL;
    return head;
}

2020062310470442.png







相关文章
|
7天前
|
存储 缓存 前端开发
【数据结构/C语言】深入理解 双向链表
【数据结构/C语言】深入理解 双向链表
|
6天前
|
算法 Java
Java数据结构与算法:双向链表
Java数据结构与算法:双向链表
|
6天前
|
算法 Java
Java数据结构与算法:循环链表
Java数据结构与算法:循环链表
|
7天前
|
算法
【数据结构与算法 刷题系列】求带环链表的入环节点(图文详解)
【数据结构与算法 刷题系列】求带环链表的入环节点(图文详解)
|
7天前
|
算法
【数据结构与算法 刷题系列】判断链表是否有环(图文详解)
【数据结构与算法 刷题系列】判断链表是否有环(图文详解)
|
7天前
|
算法
【数据结构与算法 经典例题】随机链表的复制(图文详解)
【数据结构与算法 经典例题】随机链表的复制(图文详解)
|
7天前
|
算法 C语言
【数据结构与算法 经典例题】链表的回文结构(图文详解)
【数据结构与算法 经典例题】链表的回文结构(图文详解)
|
7天前
|
算法
【数据结构与算法 经典例题】反转链表(图文详解)
【数据结构与算法 经典例题】反转链表(图文详解)
|
7天前
|
算法 C语言
【数据结构与算法 经典例题】相交链表求交点
【数据结构与算法 经典例题】相交链表求交点
|
7天前
|
算法
【数据结构与算法 刷题系列】移除链表元素
【数据结构与算法 刷题系列】移除链表元素