一、移除链表元素
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):待删除的节点非连续分布
情况(2):考虑尾节点,也考虑到了连续排分布
经过分析下来,我们发现情况(2)全部符合情况(1)的代码
情况(3):考虑头节点也是要删除的节点
二、 反转链表
1. leetcode 206
题目要求:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表的表头。
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 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
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):节点的总数是奇数
情况(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
题目要求:将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
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 的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
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. 谢谢观看哟~