链表必刷题一

简介: 链表必刷题一

一、移除链表元素



1. leetcode 203


题目要求:


指定一个元素,移除链表中与之对应的所有元素。


2.代码实现


class Solution {
    public ListNode removeElements(ListNode head, int val) {
        //链表为空
        if(head == null){
            return null;
        }
        //创建两个指针,一个用来遍历,一个用来充当前驱
        ListNode cur = head.next;
        ListNode prev = head;
        while(cur != null){
            if(cur.val == val){
                prev.next = cur.next;
            }else{
                prev = cur;
            }
             cur = cur.next;
        }
        //考虑头结点
        if(head.val == val){
            return head.next;
        }
        return head;
    }
}


3. 代码分析


思路:

创建两个指针,一个是 prev,称为前驱指针,用来串联下一个节点;另一个是 cur,用来遍历整个链表,同时在跑的过程进行 val 值的判断。


下面是几种必须要考虑到的情况:


情况(1):待删除的节点非连续分布


f96df3a3322b4b3ea29aceecd784c35e.png


情况(2):考虑尾节点,也考虑到了连续排分布


经过分析下来,我们发现情况(2)全部符合情况(1)的代码


b3d1c07e0903476a8978f6d8bd0005e6.png


情况(3):考虑头节点也是要删除的节点


8defec5333864a02b0c28231baeeb059.png


二、 反转链表


1. leetcode 206


题目要求:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表的表头。


1f478e8de6ee446ba65c5af5b2445074.png


2. 代码实现


class Solution {
    public ListNode reverseList(ListNode head) {
        if(head == null){
            return null;
        }
        ListNode prev = null;
        ListNode cur = head;
        while(cur != null){
            ListNode cur2 = cur.next;
            cur.next = prev;
            prev = cur;
            cur = cur2;
        }
        return prev;
    }
}


3. 代码分析


思路


(1)创建两个指针,一个指针是 prev,最初指向 null,之后用来串联反转后的节点;另一个指针是 cur,最初指向 head,之后用来遍历整个链表中的节点。prev 和 cur不断地向后走,当 cur == null 的时候,那么就停下来,此时的 head 就是 prev。

如下两行代码可以实现上述操作


cur.next = prev;
prev = cur;


(2)然而,此题的关键是,如何让 prev 和 head 正确地往链表后面跑。

因为上面两行代码会改变 cur 的指向,也就是说,cur 不再能够按照原先的路线跑了,基于这个原因,我们要创建一个新的指针 cur2,这个指针的作用就是在反转操作前,把 cur 的下一个位置存起来,并放在 while 循环的第一行,这样一来,就可以正确遍历了。


ListNode cur2 = cur.next;


下面的图辅助理解:


ee297b658f3d4cbaa57cbeb8c22f8719.png


三、链表的中间节点


1. leetcode 879


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


0ebcba39b62a438f8440fcb519e2e96f.png


2. 代码实现


class Solution {
    public ListNode middleNode(ListNode head) {
        //创建快慢指针
        ListNode fast = head;
        ListNode slow = head;  
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }
}


3. 代码分析


思路:


创建两个指针,一个是 fast,一次走两步,另一个是 slow,一次走一步。两个指针同时移动,当 fast 停下来的时候,slow 就是中间节点。原因就是:fast 更快,走的更远,fast 速度是 slow 的一半,而 slow 对应的路程是 fast 的一半。


fast = fast.next.next;
slow = slow.next;


此外我们应该注意:while 循环的条件应写成( fast != null && fast.next != null),而不应该写成(fast.next != null && fast != null),因为考虑到当 fast == null 时,fast.next 就会造成空指针异常。


情况(1):节点的总数是奇数


d237a9eecc2147ca9066f8a3036d8772.png


情况(2):节点的总数是偶数


50bfd6d769c84348baaa62d46b4bfe35.png


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


1. 牛客网链接


题目要求:输入一个链表,输出该链表中倒数第k个结点。

OJ链接


63fc8933beb14f3c8c9431c1241db79e.png


2. 代码实现


public class Solution {
    public ListNode FindKthToTail(ListNode head,int k) {
        //空链表和k<=0,不满足要求,返回 null
        if(head == null || k <= 0){
            return null;
        }
        ListNode fast = head;
        ListNode slow = head;
        //让 fast 先走
        for(int i = 0; i < k-1; i++){
            fast = fast.next;
            //下面判断是否 k > 链表长度
            if(fast == null){ 
                return null;
            }
        }
    //fast 和 slow 同步走
        while(fast.next != null){
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
}


3. 代码分析


这道题目,我们先理解一个例子,再阐明思路。


例子如下,假设我们要找的是 k = 2,即对应下图的倒数第二个节点。现在我们分三步走:


(1)创建快慢指针 fast 和 slow 为头节点

(2)slow 不动,fast 先走 k - 1 步

(3)在(1)的基础上,fast 和 slow 每次走一步,直到 fast.next == null 的时候,那么 slow 指向的位置就是 k = 2 的位置


53053b3a2a114cb1bc63cdec4c895383.png


思路:

如上图的例子,假设正常情况下,链表长度 length = 5


当 k = 1,k - 1 = 0

当 k = 2,k - 1 = 1

当 k = 3,k - 1 = 2

当 k = 4,k - 1 = 3

当 k = 5,k - 1 = 4


k - 1 就是我们 fast 要先走的路程,之后 fast 和 slow 指针再保持一步一步走,最后通过判断 fast.next 是否为 null即可。


因为我们知道 slow 指针是每次只能走一步的,那么我们只能通过设计 fast 路程来保持后面的 fast 和 slow 路程的同步!也就是说目标是 slow,而 fast 是用来实现 slow 的。

读者可以自己试一试这一规律,这很有意思。


此外,我们应该考虑三种特殊情况:

(1)链表为空

链表为空,无论 k 的值是多少,自然就返回 null


(2)k <= 0

假设 k = -1,链表有倒数第 -1 个节点吗?这个时候肯定也返回 null


(3)k > 链表的长度 length

假设 length = 5,k = 6,那么,说让你找链表中倒数第 6 个节点,自然也是找不到的,所以返回 null


针对情况(1)和(2),以下代码可以解决


if(head == null || k <= 0){
  return null;
}


针对情况(3),有两种解决方法


方法1: 通过遍历链表,得出 length 的值,如果用 if 判断的结果是(length - k < 0),那么就返回 null.


方法2: 在 fast 走 k - 1 步的时候,我们在 for 循环中进行判断,如果 fast == null,那么就返回 null


读者们应该更为理解第①种解决方案,而我当初做题也是这么想的,可是第一种方案要多遍历链表一次,也就是说效率更低,时间复杂度更大。所以接下来利用图解阐明第②种解决方案。


f9f95e3bd00547fda7efeee5a97583fe.png


如上图分析,假设链表长度为 5,k = 6,那么 k - 1 = 5,经过之前的分析,我们要先让 fast 指针先往后跑 5 步,以此造成的结果就是,fast 已经指向 null,所以就更别谈让后面的 slow 继续跑了,所以直接 return null。假设 k = 7,8,9,那么可想而知,fast 都不知道跑哪去了。

所以经过分析,这种表达方式就是在于表明 k 不能大于链表长度,一旦 k > 链表长度,fast 直接为 null.


五、合并两个有序链表


1. leetcode 21


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


8eff97e6d33947d3884d3e914b3cfd61.png


2. 代码实现


class Solution {
    public ListNode mergeTwoLists(ListNode head1, ListNode head2) {
        //创建一个傀儡节点
        ListNode newhead = new ListNode(-1);
        ListNode cur = newhead;
        //开始循环,并判断傀儡节点连接的下一个节点是属于哪一个链表
        while(head1 != null && head2 != null){
            if(head1.val <= head2.val){
                cur.next = head1;
                cur = cur.next;
                head1 = head1.next;
            }else{
                cur.next = head2;
                cur = cur.next;
                head2 = head2.next;
            }
        }
        //如果链表1本身为空,或者链表1走完了,就走链表2
        if(head1 == null){
            cur.next = head2;
        }
        //如果链表2本身为空,或者链表2走完了,就走链表1
        if(head2 == null){
            cur.next = head1;
        }
        return newhead.next;
    }
}


3. 代码分析


思路:


(1) 创建一个虚拟的傀儡节点 newhead,newhead 里面存的是数据域是 -1,指针域 next 是 null。目的是用来将两个链表串联起来,此外还应该创建一个 cur 充当傀儡节点的替身,因为最后的 newhead 要充当新的头结点,所以要保持 newhead 的位置不能动。


(2)head1 和 head2 每经过一个节点的时候,就比较两个节点中存的 val 值,由于此题是排序是升序的情况,那么谁的值小,就放在新的链表前面。


(3)最后考虑特殊情况


① 原始的两个链表是否都为空,是否某个链表为空,另一个链表不为空。

② 当其中的一个链表元素已经走完了,那么下一步该怎么办。


基于以上两种特殊的情况,首先我们应该确定好 while 循环的条件是 (head1 != null && head2 != null),其次我们应该添加两个 if 的条件,代码如上,我已经标出注释。


下图辅助理解:


7c64df0a72b2471d83aaaedf9a399032.pngdefbf5c59a4f4afa8f9129c64cefa77f.png



六 、分割链表


1. 牛客网链接

题目要求:

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


649a677ce66c45b69aa91a0a2b0ba14b.png


2. 代码实现


public class Partition {
    public ListNode partition(ListNode head, int x) {
        //1. 创建五个指针
        //创建两个傀儡节点 newhead1 和 newhead2
        //再创建这两个傀儡节点的替身,cur1,和 cur2
        //创建一个指针 sign 用来遍历原来的链表
        ListNode newhead1 = new ListNode(-1);
        ListNode newhead2 = new ListNode(-2);
        ListNode cur1 = newhead1;
        ListNode cur2 = newhead2;
        ListNode sign = head;
        //2. 进行区间划分
        //比 x 值小的放在前一个区间比 x 值大的放在后一个区间
        while(sign != null){
            if(sign.val < x){
                cur1.next = sign;
                cur1 = cur1.next;
                sign = sign.next;
            }else{
                cur2.next = sign;
                cur2 = cur2.next;
                sign = sign.next;
            }
        }
        //3. 傀儡节点失效,重新定义两个区间的头结点
        newhead1 = newhead1.next;
        newhead2 = newhead2.next;
        //4. 连接两个区间
        cur1.next = newhead2;
        //5. 考虑特殊情况
        if(newhead2 != null){
            cur2.next = null;
        }
        if(newhead1 == null){
            return newhead2;
        }
        //左右区间都为空
        return newhead1;
    }
}


3. 代码分析


思路:

我们先来看看正常情况下的思路分析,注意,这里给的 x 值,可以是非链表元素中的 val


(1) 创建五个指针


① 创建两个虚拟的傀儡节点 newhead1 和 newhead2,其分别存放数据域和指针域。newhead1 中存放的是 -1 和 null,newhead2 存放的是 -2 和 null。目的是用来分割链表,当节点对应的 val < x 的时候,我们把这个节点放在前一个区间,也就是属于 newhead1 节点的区间;同理,当节点对应的 val > x 的时候,我们把这个节点放在后一个区间,也就是属于 newhead2 节点的区间。


② 与此同时,我们再创建这两个傀儡节点的替身,cur1 和 cur2,让 cur1 和 cur2 起到串联节点的作用,因为在程序的最后,我们要对 newhead1 和 newhead2 头节点进行操作,所以作为两段区间新的头结点,所以这两个位置不能动它,我们只能让 cur1 和 cur2去跑。


③ 创建一个指针 sign 用来遍历原来的链表,拿 sign.val 和 x 进行比较,小的放在前一区间,大的放在后一区间。


(2)当区间划分完之后,我们要注意真正的头节点位置,两个傀儡节点在原先创建的时候,它们之中默认指向的是 null,而经过串联新的节点之后,其对应的 next 域可能会发生改变,所以我们实现如下两行代码。这表示:有节点就指向下一个节点的地址,无节点,就指向 null。这一步很关键!


newhead1 = newhead1.next;
newhead2 = newhead2.next;


给大家展示清晰的傀儡节点的内部结构,和创建普通的节点一样,只要创建了一个新的节点,其节点本身就会被系统分配内存,其中有个数据域,有一个指针域,而因为它是孤立的,所以它们的指针域指向 null


1299f5f12a354846b8afe44e1d706560.png


下面请看我的分析,我给出了一个链表,假设我们将 x = 6 设置成中间线。

x < 6 的放在前一个区间,这个区间是通过傀儡节点 newhead1 引导的;x > 6 的放在后一个区间,这个区间是通过傀儡节点 newhead2 引导的。


5dc78969ff61410f967d91f7088575a7.png


下图辅助理解:


2e0430a7145e4feaad76f074482bcb69.pngfea87536054040bf9bffdf905856a9e5.pngc3b23db404464a128133d03eefd06cd2.png




(3)上面的是普遍情况,我们再看看几个特殊情况,这依然很关键:


1df735cfc4db43ac92d7c7b109a927f4.png


请读者对照上图序号并看下面我给出的列举,我为大家列出来了所有情况,分别是:


① 正常情况


② 前一个区间为空,后一个区间不为空

我们只要设置相关条件,并满足使用代码即可


cur2.next = null; //尾节点置空
return newhead2 //将后一区间的头节点作为返回节点

③ 后一个区间为空,前一个区间不为空

我们无须设置相关条件,因为在之前有一行代码已经满足要求

cur1.next = newhead2 <=> null

想象一下,后一区间为空,那么 newhead2 == null ,这就导致了前一区间的尾节点变成了空,符合链表要求。


④ 两个区间都为空

我们无须设置相关条件

因为:不管是 return newhead1 还是 return newhead2 都为空


虽然牛客网官方定义这道题较难,但我觉得只是很多人在做题的时候失去了耐心,只要将图画好,就能迎刃而解!


f49f1602dc5d4cb7aa57f9cbd576d0b5.jpg


Over. 谢谢观看哟~


目录
相关文章
|
人工智能 算法
【数据结构算法篇】链表面试必刷题4—链表中倒数第k个结点
输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。
【数据结构算法篇】链表面试必刷题1——反转链表
给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。
|
算法
链表必刷题二
链表必刷题二
50 0
链表必刷题二
|
7月前
【移除链表元素】LeetCode第203题讲解
【移除链表元素】LeetCode第203题讲解
|
6月前
|
存储 SQL 算法
LeetCode力扣第114题:多种算法实现 将二叉树展开为链表
LeetCode力扣第114题:多种算法实现 将二叉树展开为链表
|
6月前
|
存储 SQL 算法
LeetCode 题目 86:分隔链表
LeetCode 题目 86:分隔链表
|
6月前
|
存储 算法 Java
【经典算法】Leetcode 141. 环形链表(Java/C/Python3实现含注释说明,Easy)
【经典算法】Leetcode 141. 环形链表(Java/C/Python3实现含注释说明,Easy)
60 2
|
7月前
<数据结构>五道LeetCode链表题分析.环形链表,反转链表,合并链表,找中间节点.
<数据结构>五道LeetCode链表题分析.环形链表,反转链表,合并链表,找中间节点
53 1
|
6月前
|
算法
【经典LeetCode算法题目专栏分类】【第7期】快慢指针与链表
【经典LeetCode算法题目专栏分类】【第7期】快慢指针与链表