链表必刷题一

一、移除链表元素

1. leetcode 203

题目要求:
指定一个元素,移除链表中与之对应的所有元素。
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):待删除的节点非连续分布
步骤分析
情况(2):考虑尾节点,也考虑到了连续排分布
经过分析下来,我们发现情况(2)全部符合情况(1)的代码
步骤分析
情况(3):考虑头节点也是要删除的节点
步骤分析

二、 反转链表

1. leetcode 206

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

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;

下面的图辅助理解:
分析
分析

三、链表的中间节点

1. leetcode 879

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

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):节点的总数是奇数
1
情况(2):节点的总数是偶数
2

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

1. 牛客网链接

题目要求:输入一个链表,输出该链表中倒数第k个结点。
OJ链接
在这里插入图片描述

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 的位置
分析
思路:
如上图的例子,假设正常情况下,链表长度 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

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

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

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 的条件,代码如上,我已经标出注释。

下图辅助理解:
分析一
分析二

六 、分割链表

1. 牛客网链接

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

OJ链接
结果

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
分析

下面请看我的分析,我给出了一个链表,假设我们将 x = 6 设置成中间线。
x < 6 的放在前一个区间,这个区间是通过傀儡节点 newhead1 引导的;x > 6 的放在后一个区间,这个区间是通过傀儡节点 newhead2 引导的。

分析
下图辅助理解:

分析
分析
分析

(3)上面的是普遍情况,我们再看看几个特殊情况,这依然很关键:
分析
请读者对照上图序号并看下面我给出的列举,我为大家列出来了所有情况,分别是:

① 正常情况

② 前一个区间为空,后一个区间不为空
我们只要设置相关条件,并满足使用代码即可

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

③ 后一个区间为空,前一个区间不为空
我们无须设置相关条件,因为在之前有一行代码已经满足要求
cur1.next = newhead2 <=> null
想象一下,后一区间为空,那么 newhead2 == null ,这就导致了前一区间的尾节点变成了空,符合链表要求。

④ 两个区间都为空
我们无须设置相关条件
因为:不管是 return newhead1 还是 return newhead2 都为空

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

结尾
Over. 谢谢观看哟~

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十七ing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值