赞
踩
题目要求:
指定一个元素,移除链表中与之对应的所有元素。
leetcode 203
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;
}
}
思路:
创建两个指针,一个是 prev,称为前驱指针,用来串联下一个节点;另一个是 cur,用来遍历整个链表,同时在跑的过程进行 val 值的判断。
下面是几种必须要考虑到的情况:
情况(1):待删除的节点非连续分布
情况(2):考虑尾节点,也考虑到了连续排分布
经过分析下来,我们发现情况(2)全部符合情况(1)的代码
情况(3):考虑头节点也是要删除的节点
题目要求:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表的表头。
leetcode 206
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;
}
}
思路:
(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;
下面的图辅助理解:
题目要求:给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
leetcode 879
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;
}
}
思路:
创建两个指针,一个是 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个结点。
OJ链接
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;
}
}
这道题目,我们先理解一个例子,再阐明思路。
例子如下,假设我们要找的是 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.
题目要求:将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
leetcode 21
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;
}
}
思路:
(1) 创建一个虚拟的傀儡节点 newhead,newhead 里面存的是数据域是 -1,指针域 next 是 null。目的是用来将两个链表串联起来,此外还应该创建一个 cur 充当傀儡节点的替身,因为最后的 newhead 要充当新的头结点,所以要保持 newhead 的位置不能动。
(2)head1 和 head2 每经过一个节点的时候,就比较两个节点中存的 val 值,由于此题是排序是升序的情况,那么谁的值小,就放在新的链表前面。
(3)最后考虑特殊情况
① 原始的两个链表是否都为空,是否某个链表为空,另一个链表不为空。
② 当其中的一个链表元素已经走完了,那么下一步该怎么办。
基于以上两种特殊的情况,首先我们应该确定好 while 循环的条件是 (head1 != null && head2 != null),其次我们应该添加两个 if 的条件,代码如上,我已经标出注释。
下图辅助理解:
题目要求:
现有一链表的头指针 ListNode* head,给一定值 x,编写一段代码将所有小于 x 的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
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;
}
}
思路:
我们先来看看正常情况下的思路分析,注意,这里给的 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. 谢谢观看哟~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。