基础知识
链表是一种物理存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表解题技巧
链表题可以通过画图来理清思路
链表题细节很重要,一定要好好分析,充分考虑所有情况,例如在分析链表题的时候,务必要注意讨论下一步要用到的链表节点是否为空,语句执行时是否为报空指针异常!在写每个循环时,一定要想清楚循环进入的条件是什么,退出的条件是什么!例如142题中:
while(fast != null && fast.next != null){
slow = slow.next;
//如果fast.next为空,就不存在.next,报空指针异常
//但如果是fast.next.next=null就只是fast.next的next存储的引用地址为空,不会报异常
fast = fast.next.next;
...
}
在java中,链表是通过对象模拟形成的,一个节点就是一个对象,成员变量val就是当前节点对象的值,next就是下一个节点对象的引用地址。链表定义如下:
//Definition for singly-linked list.
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
//Definition for double-linked list.
public class ListNode {
int val;
ListNode next;
ListNode prev;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
ListNode(int val, ListNode next, ListNode next) { this.val = val; this.next = next; this.prev = prev;}
}
1、在做链表题的时候,首先分析头节点会不会改变,如果改变就创建虚拟头节点,方便处理边界情况,然后根据题目情况分析是否真的需要虚拟头节点。例如删除节点的题目中就要先分析头结点会不会被删除,如果会就创建一个虚拟头节点dummy指向真实头节点;
2、快慢指针法常用来找点目标节点: 定义快指针和慢指针指向开头,然后快指针先走k步,此时快指针比慢指针快k步;然后快慢指针一起走,当快指针走到末尾(倒数第1个节点)时,慢指针刚好走到倒数第k+1个节点,从而找到目标节点(第k个节点),快慢指针关键在于两指针相距k,不在乎起点在哪。
3、链表中添加或删除节点或交换相邻节点时,先处理后面的节点(设置next指向等),再处理前面的节点。
4、一些顺序问题
链表中共有n个节点,倒数第k+1个节点就是第n-k个节点,倒数第k个节点就是第n-(k-1)个节点;
数组中共有n个元素,倒数第k+1个元素的下标就是第n-1-k个元素下标,倒数第k个元素的下标就是第n-1-(k-1)个元素下标;
5、翻转链表的问题,一般需要先确定翻转区间m,n,翻转区间内节点指向;然后根据链表特性,需要找到m-1,n+1的节点以改变其next指向;从而得到反转后的链表;如206,92.
6、链表节点预先判空遵循用到哪个节点就判断是否为空,如head = head.next
就需要保证head != null
,再如cur.next = cur.next.next
就需要保证cur != null
和cur.next != null
,否则就会报空指针异常
题目练习
19. 删除链表的倒数第 N 个结点
题目链接
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
题目分析
本题为单链表,要删除链表的倒数第 n 个结点,就需要找到地n-1个节点的后置指针(next),让该指针指向第n+1个节点就实现了删除。本题可以使用**双指针(快慢指针)**法来求解。
- 首先分析头节点会不会改变,本题可能会删除头节点,因此建立一个虚拟头节点(凡是有可能删除头结点的题目都应该创建一个虚拟头节点,这样就不用处理头节点被删除的情况),让其指向头节点;
- 定义一个快指针,从虚拟头节点开始先向后走n步;
- 再定义一个慢指针,指向虚拟头节点,此时快指针比慢指针快n步,也就是超前n个节点;
- 然后让快指针和慢指针同时向后走,直到快指针走到末尾时,慢指针应该刚好指向倒数第n+1个节点;
- 然后让慢指针指向的节点指向下一个节点的下一个节点,也就是倒数第n-1个节点,从而删除倒数第n个节点。
代码实现
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
//定义一个虚拟头节点
ListNode dummy = new ListNode(0, head);
//定义一个快慢指针指向虚拟头节点
ListNode fast = dummy, slow = dummy;
//让快指针先走n步,慢指针不动,两者相差n步
while(n-- != 0) fast = fast.next;
//然后让快慢指针同时后移,当快指针指向末尾时,慢指针指向倒数第n+1个节点
while(fast.next != null ){
fast = fast.next;
slow = slow.next;
}
//然后让慢指针指向的节点指向倒数第n-1个节点
slow.next = slow.next.next;
//头节点有可能被删掉,因此不能直接指向head
return dummy.next;
}
}
237 删除链表中的节点
题目链接
请编写一个函数,用于 删除单链表中某个特定节点 。在设计函数时需要注意,你无法访问链表的头节点 head ,只能直接访问 要被删除的节点 。
题目数据保证需要删除的节点 不是末尾节点 。
提示:
链表中节点的数目范围是 [2, 1000]
-1000 <= Node.val <= 1000
链表中每个节点的值都是唯一的
需要删除的节点 node 是 链表中的一个有效节点 ,且 不是末尾节点
题目分析
首先分析是否需要创建虚拟头节点,本题不需要,因为本题解法不涉及删除头节点后重新设定头节点很麻烦的问题。
单链表,每个节点只知道下一个节点的位置,解决此题分两步:
- 将待删除的节点的下一个节点的值赋给待删除节点;
- 让修改值后的待删除节点指向下一个节点的下一个节点;
完成指定节点的删除。
代码实现
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public void deleteNode(ListNode node) {
//既然不能删除自己,那就把自己变成另一个人
node.val = node.next.val;
//然后干掉那个人
node.next = node.next.next;
}
}
C++代码更加简单!
class Solution {
public:
void deleteNode(ListNode* node) {
//替换node指向的值为下一个node指向的值
*(node) = *(node->next);
}
};
83. 删除排序链表中的重复元素
存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素 只出现一次 。返回同样按升序排列的结果链表。
题目分析
首先判断头节点是否会发生改变,不会,因此不用创建虚拟头节点;
然后设定一个指针指向头节点,然后在指针指向的节点不为空的前提下进行判断:
- 如果指针指向的节点的下一节点为空,则令指针指向下一节点,即此时指向的节点为空;
- 如果指针指向的节点的下一节点不为空,则进一步进行判断:
- 如果下一个节点的值与当前指针指向的节点的值相同,则删除下一个点,即指向下一个节点的下一个节点;
- 如果下一个节点的值与当前指针指向的节点的值不同,则指向下一个节点;
以此不断迭代,直到指针指向的节点为空,则退出迭代,得到删除重复元素的链表。
本题的主要思想在于通过指针指向节点(当前节点),然后不断判断下一个节点是否与当前节点相等,如果相等则指向下下个节点,再进行判断;如果不等则指针后移;直到指针指向的节点为空。
代码实现
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode pointer = head;
while(pointer != null){
//如果指针指向的节点的值与下一节点的值相等,则删除下一个节点
if(pointer.next != null && pointer.next.val == pointer.val){
pointer.next = pointer.next.next;
}else{
//如果不等,则移动指针到下一个节点
pointer = pointer.next;
}
}
//头节点不可能被删除
return head;
}
}
82. 删除排序链表中的重复元素 II
存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中 没有重复出现 的数字。
返回同样按升序排列的结果链表。
题目分析
画图分析画图分析画图分析!!!
首先判断头节点是否会改变,如果头节点与其他节点重复就会被删除,因此创建虚拟头节点;
然后创建一个指针pointer指向虚拟头节点,
在保证pointer的下一节点与下下节点不为空的前提下判断下一节点与下下节点是否相同:
- 如果pointer的下一节点与pointer的下下节点相同,则记录改节点的值,然后遍历链表(因为升序排列,因此不会遍历整个链表)找到所有重复元素;
- 在pointer的下一节点不为空的前提下,找到一个重复节点就令pointer.next等于pointer.next.next,从而删除重复节点,直到pointer的下一节点为空或者节点不重复,退出循环;
- 如果pointer的下一节点与pointer的下下节点不同,说明不重复(利用链表升序的特点,下一节点和下下节点不同,则下一节点绝对不重复),则pointer后移,继续判断;
直到pointer的下一节点或pointer的下一节点的下一节点 为空时,退出循环
- 如果是pointer的下一节点为空,说明此时pointer指向的是最后一个节点,不会重复;
- 如果是pointer的下一节点的下一节点为空,说明此时pointer指向的是倒数第二个节点,不可能与最后一个节点重复,因为如果重复,就会在pointer指向倒数第三个节点时被干掉。
本方法的主要思想在于利用链表升序的特点,确立指针指向节点(当前节点)的下一节点和下下节点如果不同,则下一节点绝对不重复这一性质,从而移动指针,遍历整个链表删掉所有重复元素,需要明确的是,指针指向的节点绝对不重复,否则指针根本不会指向它。难点在于确立性质和关于节点为空的分析
代码实现
class Solution {
public ListNode deleteDuplicates(ListNode head) {
//定义一个虚拟头节点
ListNode dummy = new ListNode(0,head);
//定义一个指针指向虚拟头节点
ListNode pointer = dummy;
//因为是判断pointer.next和pointer.next.next的值是否相等,所以必须不为空
while(pointer.next != null && pointer.next.next != null){
if(pointer.next.val == pointer.next.next.val){
//如果相等,则记录元素并遍历链表删除重复元素
int repeatValue = pointer.next.val;
//删除重复元素 此处必须判断pointer.next != null 或者当链表为[1,1]时会报错
while(pointer.next != null && pointer.next.val == repeatValue){
pointer.next = pointer.next.next;
}
}else{
//如果不等,则指针后移
pointer = pointer.next;
}
}
//头节点若重复也会被删除
return dummy.next;
}
}
61. 旋转链表
题目链接
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
题目分析
看懂题目,实际就是将后k个节点移动到前面
首先判断是否需要建立虚拟头节点,不需要,因为虽然本题中头节点发生改变,但重新设定头节点很简单。
然后明确,有个坑在于k的取值,有可能超过链表长度n,处理方式是先对k取模k%=n;
保证k的取值为[0, n-1],逻辑在于旋转k次和旋转k%n次结果是一样的,如示例2中k=4,n=3,;
然后我们需要知道最后一个节点,它的next将指向旋转前的第一个节点;需要知道倒数第k+1个点,它的next将指向null;
找目标节点可以使用快慢指针法,快指针先走k步,然后快慢指针一起走,当快指针走到末尾时,慢指针刚好走到倒数第k+1个节点;
然后将快指针指向的节点的next指向修改为旋转前的头节点,旋转后的头节点为指针指向的节点的下一个节点, 慢指针指向的节点的next指向修改为null。
代码实现
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if(head == null){
return head;
}
//防止k过大超出链表长度
int length = 0;
for(ListNode p = head; p != null; p = p.next){
length++;
}
//保证k在[0,length-1]
k %= length;
//快慢指针找到倒数第K+1个节点和倒数第1个点
ListNode fast = head;
ListNode slow = head;
while(k-- != 0){
fast = fast.next;
}
//当fast走到末尾时,slow刚好走到倒数第k+1个节点
while(fast.next != null){
fast = fast.next;
slow = slow.next;
}
//修改fast指向的节点的next为原链表第一个节点 头节点为倒数第k+1个节点的下一个节点 倒数第k+1个节点的next为空
fast.next = head;
head = slow.next;
slow.next = null;
return head;
}
}
24. 两两交换链表中的节点*
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
题目分析
首先分析头节点会发生改变,创建虚拟头节点;
然后根据题意可将链表中的节点按两个一对分为多对,然后一次处理一对来做。
- 首先第一对相邻节点a,b,设定一个指针指向虚拟头节点(当前节点),然后让当前节点的next指向下一节点的下一节点(b);a节点的next指向 b节点的next指向的节点,也就是下一对节点的开头节点;b节点的next指向a节点,完成第一对的交换;然后指针后移,指向第一对节点的末尾节点,此时也就是a节点;
- 然后进行第二对节点a1,b1的交换,让当前节点的next指向下一节点的下一节点(b1),a节点的next指向b节点的next,b1节点的next指向a1节点,完成第二对的交换,然后指针后移
以此类推,完成所有相邻节点的交换(如果有节点落单则不用交换)
如果没有虚拟头节点,就需要返回第二个节点作为新的头节点,但当链表长度为1时,又不能返回第二个,这样就得进行判断,因此需要创建虚拟头节点
代码实现
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(0,head);
for(ListNode pointer = dummy; pointer.next != null && pointer.next.next != null;){
//两个一对儿a,b
ListNode a = pointer.next, b = pointer.next.next;
//当前节点的next指向b a的next指向b的next指向的节点 b的next指向a节点
pointer.next = b;
a.next = b.next;
b.next = a;
//更新pointer的指向为a,继续交换
pointer = a;
}
return dummy.next;
}
}
206. 反转链表
题目链接
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
提示:
链表中节点的数目范围是 [0, 5000]
-5000 <= Node.val <= 5000
进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?
题目分析
由于是翻转整个链表,区间天然为[head,tail],修改指向即可,最后头结点next指向为空,tail节点为新的头结点。
迭代法:
首先考虑头节点会不会改变,会,但是不需要创建虚拟头节点,因为反转后可以直接返回新的头节点。
然后,设立两个指针,编号为a,b,a指向头节点,b指向第二个节点,在b指向的节点不为空的前提下,
- 首先由于,反转两个节点需要存储第三个节点的引用,因此设立c存储b.next;
- 然后修改b.next指向为a;
- 然后指针后移,a指向b,b指向c,继续迭代
最后b指向的节点为空,退出循环,反转完成。
递归法:
递归法相对抽象一些,但其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。关键是初始化的地方不好理解, 可以看到双指针法中初始化 cur = head,pre = null,在递归法中可以看出初始化的逻辑也是一样的,只不过写法变了。
代码实现
class Solution {
//迭代法 时间复杂度O(n) 空间复杂度O(1)
public ListNode reverseList(ListNode head) {
if(head == null){
return head;
}
//设立两个指针用来反转两个节点
ListNode prev = head;
ListNode curr = head.next;
//循环条件为后一个指针curr不为空
while(curr != null){
//设立临时指针存储当前指针的下一节点引用地址
ListNode temp = curr.next;
//修改当前指针指向的节点的指向为前一指针
curr.next = prev;
//两个指针后移
prev = curr;
curr = temp;
}
//反转完成,令原来的头节点的next指向为空,否则会成为环形链表
head.next = null;
return prev;
}
}
//改进版
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null){
return head;
}
ListNode prev = null, cur = head;
while(cur != null){
ListNode tmp = cur.next;
cur.next = prev;
prev = cur;
cur = tmp;
}
return prev;
}
}
class Solution {
//递归法实现 时间复杂度:O(n) 空间复杂度O(n)
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
public ListNode reverse(ListNode prev, ListNode cur){
//递归终止条件
if(cur == null){
return prev;
}
//递归主体 更改节点next指向
ListNode tmp = cur.next;
cur.next = prev;
// prev = cur;
// cur = cur.tmp;
return reverse(cur, tmp);
}
}
92. 反转链表 II ***
题目链接
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
提示:
链表中节点数目为 n
1 <= n <= 500
-500 <= Node.val <= 500
1 <= left <= right <= n
进阶: 你可以使用一趟扫描完成反转吗?
题目分析
首先分析,头节点可能会发生变化,创建虚拟头节点;
- 如果没有设立虚拟头节点,当头节点在mn区间内,就需要返回n对应的节点也就是d,如果不在就返回原头节点,需要额外的边界判断,因此需要设立虚拟头节点;
然后需要求出上图中abcd这四个点的位置
- a点就是从虚拟头节点走m-1步;
- b点就是a点的next;
- d点就是从虚拟头节点走n步;
- c点就是d点的next;
然后根据上一题的做法反转m到n之间的链表,
- 设立prev,curr两个指针,prev指向b节点,curr指向b节点的下一节点,创建临时节点temp存储curr.next对应的节点;
- 将curr指向的节点的next修改为prev;
- prev和curr指针后移,继续执行循环,直到curr指针指向c节点
然后修改a的next指向d节点,b的next指向为c节点,反转完毕;
代码实现
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
if(left == right){
return head;
}
ListNode dummy = new ListNode(0,head);
//确定abdc四个点的位置
ListNode a = dummy, d = dummy;
//a点就是从dummy走left-1步
for(int i = 0; i < left - 1; i++) a = a.next;
//b点就是a的next指向的节点
ListNode b = a.next;
//d点就是从dummy走right步
for(int i = 0; i < right; i++) d = d.next;
//c点就是d的next指向的节点
ListNode c = d.next;
//反转bd之间的节点 思考输入[1,2,3,4,5] 5 5时的情况
for(ListNode prev = b, curr = b.next; curr != c;){
ListNode temp = curr.next;
curr.next = prev;
prev = curr;
curr = temp;
}
//连接翻转后的子序列
//修改a节点的next指向为d b节点的next指向为c
a.next = d;
b.next = c;
return dummy.next;
}
}
160. 相交链表
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构
提示:
listA 中节点数目为 m
listB 中节点数目为 n
0 <= m, n <= 3 * 10^4
1 <= Node.val <= 10^5
0 <= skipA <= m
0 <= skipB <= n
如果 listA 和 listB 没有交点,intersectVal 为 0
如果 listA 和 listB 有交点,intersectVal = listA[skipA + 1] = listB[skipB + 1]
进阶:你能否设计一个时间复杂度 O(n) 、仅用 O(1) 内存的解决方案?
题目分析
首先头节点不会改变,不需要创建虚拟头节点;
然后题目中给出条件,链表中不存在环,因此两链表只有两种情况,一无交点,二只有一个交点,可以按下图考虑
设定两个指针p,q分别指向两个链表的头节点,然后同步移动,当p走完链表1后,就从链表2的头节点开始走;同理,当q走完链表2的后,就从链表1的头节点开始走;
如果两链表不相交,则两个指针分别走完a+b步后都为null,可以认定为此时两指针指向的节点的值相等;
如果两链表相交,当a=b,则两个指针必然在两链表交汇处相遇;如果a!=b,则两个指针分别走完a+b+c步后在两链表交汇处相遇;不论哪种情况,两个指针的第一次相遇都只会在两链表交汇处,此时两指针指向的节点的值相等。
根据上述特点,当两个指针相遇,也就是两个指针指向的节点相等时,即可返回两链表交汇处的值。
时间复杂度与链表的长度有关,两链表总长度为a+b=n时,为O(n),其中 a 和 b是分别是链表 headA 和 headB 的长度,两个指针同时遍历两个链表,每个指针遍历两个链表各一次。
空间复杂度为O(1)。
代码实现
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//必须用两指针,直接用headA,headB遍历会得到空
ListNode p = headA, q = headB;
//极端情况下,两链表都只有1个节点,值为5,即相交于5这个节点
while(p != q){
//注意,不能写if(p.next == null),这样不相交时,两指针永远也不会相遇,陷入死循环
//考虑输入为0 [2,6,4] [1,5] 3 2
//p指针走完链表A后从链表B头节点开始走
if(p == null){
p = headB;
}else{
p = p.next;
}
//q指针走完链表B后从链表A头节点开始走
if(q == null){
q = headA;
}else{
q = q.next;
}
}
//退出循环后p,q指向的节点的值必然相等(都为空也认为相等)
return q;
}
}
142. 环形链表 II ***
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
进阶:你是否可以使用 O(1) 空间解决此题?
提示:
链表中节点的数目范围在范围 [0, 10^4] 内
-10^5 <= Node.val <= 10^5
pos 的值为 -1 或者链表中的一个有效索引
题目分析
首先,本题中头节点不发生变化,因此不用创建虚拟头节点。
第一轮,设定快慢指针指向头节点,然后让慢指针走一步,快指针走两步,则有当慢指针走x步到b点时,快指针走了2x步;此时假定快慢指针在c点第一次相遇,那么当慢指针走到b点时,快指针必然在环上c点的对称点c’(与b点相距y步),因为此时慢指针相距c点y步,快指针比慢指针快两倍,也就是相距c点2y步,这样才会有快慢指针在c点两遇;
第二轮,当快指针和慢指针在c点相遇后,令慢指针指向a点,与快指针同步前进,当慢指针走到b点时,快指针也必然刚好走到b点;因为在第一轮中,慢指针走x步到达b点,快指针走2x步到达c’点,说明从b到c‘刚好为x步,b距离c有y步,c‘距离b也是y步,因此从c到b也是x步。故而当快指针从a点开始走,慢指针从c点开始走,都走x步,必然在b点相遇,从而找到入环的第一个节点b。
本题主要思想是,通过第一轮可以找到c点,c点的特性在于距离b点和a点距离b点都为x步,因此可以通过第二轮确定b点。
另外,如果不存在环,就会出现快指针指向为空,直接返回null即可。
务必考虑清楚为空的情况!!!(又忽略了555)
代码实现
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
//快慢指针法 时间复杂度为O(n) 空间复杂度为O(1)
public ListNode detectCycle(ListNode head) {
//1个结点也可能有环
if(head == null) return head;
ListNode fast = head, slow = head;
//1.判断是否有环,快指针走两步,慢指针走一步,无环fast会为null
while(fast != null){
//注意非空判断,不能直接写fast=fast.next.next
fast = fast.next;
slow = slow.next;
if(fast != null){
fast = fast.next;
}else{
//必须加这句,
//考虑输入[1],无环的情况,会执行下方slow=slow.next,报空指针异常
return null;
}
//2.判断两指针是否相遇,相遇则两指针重新遍历,寻找入环点b
if(fast == slow){
fast = head;
//fast从head出发和slow从相遇点出发,同时前进,再次相遇必然是b点
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
//无环返回空
return null;
}
}
148. 排序链表 *****
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
进阶:你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?
提示:
链表中节点的数目在范围 [0, 5 * 10^4] 内
-10^5 <= Node.val <= 10^5
题目分析
快速排序时间复杂度为O(logn),但空间复杂度为O(logn),不满足要求,
归并排序(自顶向下)时间复杂度:O(nlogn),其中 n 是链表的长度;空间复杂度:O(logn),其中 n是链表的长度,空间复杂度主要取决于递归调用的栈空间;也不满足要求。
本题的解法只有一个,即自底向上归并排序。
归并排序一般是自上而下,即先通过递归的方式不断二分,直到子序列只剩两个元素,完成排序,然后按规则归并,得到最终的排序结果。
自底向上则是直接两两一组归并,然后每轮增加子链表长度,直到子链表长度与原链表长度相等,排序完成。
首先头节点会发生改变,创建虚拟头节点。
然后,求得链表的长度 length,并将链表拆分成子链表进行多轮归并:
- 第一轮将链表分为若干个长度为1的子链表,进行分组归并(每组两个子链表),按规则归并后的结果必然是小数在前,大数在后;
- 第二轮将链表分为若干个长度为2的子链表,然后对每一段子链表进行归并;
- 第三轮将链表分为若干个长度为4的子链表,进行分组归并;
- 第四轮将链表分为若干个长度为8的子链表,进行分组归并;
- 。。。
- 第m轮将链表分为若干个长度为2^m的子链表,进行归并;
直到子链表长度>=链表长度,排序完成。
另外,当链表长度n不是2的整数次幂时,最后一组归并时的第2个子链表特殊,遍历到空时需提前结束。
整个链表总共要遍历logn次,每次遍历的复杂度为O(n),因此总的时间复杂度为O(nlogn);由于只使用循环,没有使用数组,系统栈以及递归等,只会使用常数个变量,因此空间复杂度为O(1)。
代码实现
class Solution {
public ListNode sortList(ListNode head) {
if (head == null) {
return head;
}
//计算链表长度
int length = 0;
ListNode node = head;
while (node != null) {
length++;
node = node.next;
}
ListNode dummyHead = new ListNode(0, head);
//划分子链表(1,2,4,8,...,length)进行多轮归并排序,当子链表长度等于原链表长度时,排序完成 分
for (int subLength = 1; subLength < length; subLength <<= 1) {
//prev用于归并后的链表连接,
//curr用于确定本轮中每组排序时两子链表的边界(一轮有m个子链表,两个为一组,共进行m/2组)
ListNode prev = dummyHead, curr = dummyHead.next;
while (curr != null) {
//定义指针head1指向第1个子链表起始位置并通过curr确定第1个子链表边界
ListNode head1 = curr;
for (int i = 1; i < subLength && curr.next != null; i++) {
curr = curr.next;
}
//定义指针head2指向第2个子链表起始位置并通过curr确定第2个子链表边界
ListNode head2 = curr.next;
curr.next = null; //截断第1个子链表(尾节点指向null)
curr = head2; //开启第2个子链表
//注意:链表长度n不是2的整数次幂时,第2个子链表可能特殊,遍历到空时需提前结束
for (int i = 1; i < subLength && curr != null && curr.next != null; i++) {
curr = curr.next;
}
//next记录下一组的第1个子链表的起始位置
//需保证curr不为空(否则可能报空指针异常),next为空(否则可能会指向之前的节点)
ListNode next = null;
if (curr != null) {
next = curr.next;
curr.next = null; //截断第2个子链表(尾节点指向null)
}
//归并两个子链表并得到归并后链表的头节点
ListNode mergedListHead = merge(head1, head2);
//通过prev将归并后的链表添加进新链表,最后会得到完成归并排序的链表
prev.next = mergedListHead;
//保证prev指向已归并链表的最后一个节点
while (prev.next != null) {
prev = prev.next;
}
//后移,开始新一组的两子链表归并 直到curr指向为null说明超出链表,完成本轮归并排序
curr = next;
}
}
return dummyHead.next;
}
//归并 治 由于从子链表长度从1开始,因此每次传入的子链表必然是已排序好的,只需按规则归并即可
public ListNode merge(ListNode head1, ListNode head2) {
ListNode dummyHead = new ListNode(0);
ListNode tempList = dummyHead, p1 = head1, p2 = head2;
//定义两个指针分别指向两个子链表,在两指针都不越界的情况下
while (p1 != null && p2 != null) {
//如果p2指向的元素大于p1,则p1在前,存入新链表,p1后移,继续判断
//反之同理,直到有指针越界
if (p1.val <= p2.val) {
tempList.next = p1;
p1 = p1.next;
} else {
tempList.next = p2;
p2 = p2.next;
}
tempList = tempList.next; //后移以便存入下一个节点
}
//当有指针越界时,若p1指针未越界,p2指针越界,说明此时p1负责的已排序的链表元素还未归并完,继续添加进新链表
//反之同理,直到归并完成,指针指向为空
if (p1 != null) {
tempList.next = p1;
} else if (p2 != null) {
tempList.next = p2;
}
return dummyHead.next;
}
}
练习
左程云视频讲解
例1 回文链表问题:
27.回文链表
笔试可以直接将链表元素依次压入栈,然后遍历链表,并依次弹出栈中元素进行判断,时间和空间复杂度O(n),
class Solution {
public boolean isPalindrome(ListNode head) {
Stack<ListNode> stk = new Stack<ListNode>();
ListNode cur = head;
//链表入栈
while(cur != null){
stk.push(cur);
cur = cur.next;
}
//遍历链表元素并与弹出的栈顶元素进行比较
while(head != null){
if(head.val != stk.pop().val){
return false;
}
head = head.next;
}
return true;
}
}
class Solution {
private ListNode frontPointer;
public boolean isPalindrome(ListNode head) {
frontPointer = head;
return recursivelyCheck(head);
}
//递归,实际就是通过栈实现,每一次前递就是压入元素进栈,每一次回归就是弹出栈顶元素进行判断
public boolean recursivelyCheck(ListNode currentNode){
//递归终止条件 当前节点为空,说明到了最后,返回true,开始回归
if(currentNode != null){
//进行递归 还未遍历完节点就返回false
if(!recursivelyCheck(currentNode.next)){
return false;
}
//最后一层前递中,尾结点的下一节点为空,
//开始回归,执行判断,如果不一样直接返回false,程序结束
if(currentNode.val != frontPointer.val){
return false;
}
//如果相等,则继续回归判断下一个,
frontPointer = frontPointer.next;
}
return true;
}
}
而如果是面试时就可以考虑时间复杂度为O(N),额外空间复杂度为O(1)的算法,通过快慢指针实现。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
if(head == null || head.next == null){
return true;
}
//快慢指针
ListNode slow = head;
ListNode fast = head;
//找到中间节点 当快指针到终点时,慢指针到中点
//注意此处条件的设定,
//链表长度为偶数时fast走到倒数第二个节点,slow走到中间两节点的第一个,如1 2 3 3 2 1
//为奇数时fast走到最后1个节点,slow走到中点,如1 2 3 2 1
while(fast.next != null && fast.next.next != null){
slow = slow.next;
fast = fast.next.next;
}
//反转后半链表
fast = slow.next; //令快指针指向后半部第1个节点
slow.next = null; //令中间节点的指向为空
ListNode temp = null;
while(fast != null){
temp = fast.next;
fast.next = slow; //改变指向
//后移继续
slow = fast;
fast = temp;
}
//反转结束后 slow指向最后1个节点,令fast指向第1个节点
temp = slow;
fast = head;
//判断是否为回文链表
boolean result = true;
while(slow != null && fast != null){
if(slow.val != fast.val){
result = false;
}
slow = slow.next; //右->中
fast = fast.next; //左->中
}
//恢复链表
slow = temp.next; //令slow指向倒数第2个节点
temp.next = null;
//当slow指向中间节点时,fast为空,从而slow为空,退出循环
while(slow != null){
fast = slow.next;
slow.next = temp; //恢复指向
//前移继续
temp = slow;
slow = fast;
}
return result;
}
}
例2 将单向链表按某值划分成左边小、中间相等、右边大的形式
相关题目:86分隔链表
普通方法(不保证相对顺序),将链表节点放到以链表节点为元素的数组(ListNode[] array = new ListNode[listLength];
),然后对数组进行partition(荷兰国旗问题) 时间复杂度为O(N) ,额外的空间复杂度为O(N)
进阶方法(保证相对顺序),将链表划分成三个子链表,然后合并,时间复杂度为O(N) ,额外的空间复杂度为O(1)
public class SmallerEqualBigger {
public static void main(String[] args) {
ListNode listNode5 = new ListNode(55);
ListNode listNode4 = new ListNode(6,listNode5);
ListNode listNode3 = new ListNode(8,listNode4);
ListNode listNode2 = new ListNode(72,listNode3);
ListNode listNode1 = new ListNode(9,listNode2);
ListNode head = new ListNode(3,listNode1);
Solution solution = new Solution();
System.out.println(solution.listPartition(head, 8));;
}
}
class Solution {
public static ListNode listPartition(ListNode head, int pivot){
ListNode sH = null; //small head
ListNode sT = null; //small tail
ListNode eH = null;
ListNode eT = null;
ListNode bH = null;
ListNode bT = null;
ListNode next = null;
//遍历链表
while(head != null){
next = head.next;
head.next = null;
if(head.val < pivot){
if(sH == null){
sH = head;
sT = head;
}else {
sT.next = head; //修改指向
sT = head;
}
}else if(head.val == pivot){
if(eH == null){
eH = head;
eT = head;
}else {
eT.next = head;
eT = head;
}
}else {
if(bH == null){
bH = head;
bT = head;
}else {
bT.next = head;
bT = head;
}
}
//后移
head = next;
}
//如果有小于区就连接等于区,如果等于区存在就连接大于区,否则小于区去连
if(sT != null){
sT.next = eH;
eT = eT == null ? sT : eT; //确定谁连大于区
}
//连接大于区
if(eT != null){
eT.next = bH;
}
return sH != null ? sH : (eH != null ? eH : bH);
}
}
例3 复制含有随机指针节点的链表
题目链接
普通解法,需要使用到哈希表(HashMap)结构,首先将链表每个节点复制一份并放在map中,key为老节点,value为新节点;然后遍历老链表每个节点cur获取next和randmap.get(cur.next)
map.get(cur.rand)
并赋给新链表,,时间复杂度为O(N),额外空间复杂度为O(N)。
进阶解法:在原来链表的基础上修改,不使用额外的数据结构,只用有限几个变量;首先生成克隆节点,放在老链表的下一个,然后克隆节点去连上老链表的下一个节点,然后把原节点和克隆节点单独拿出来,由图可知原节点rand指针指向的节点的下一节点就是克隆节点的rand的指向,依次类推,最后分离新老链表即可;时间复杂度为O(N),额外空间复杂度为O(1)。
代码实现
/*
public class RandomListNode {
int label;
RandomListNode next = null;
RandomListNode random = null;
RandomListNode(int label) {
this.label = label;
}
}
*/
import java.util.*;
public class Solution {
public RandomListNode Clone(RandomListNode head) {
if (head == null) return null;
RandomListNode dummy = new RandomListNode(-1);
dummy.next = head;
//对原链表的每个节点进行复制,并追加到原节点的后面;
//完成后,链表的奇数位置代表了原链表节点,偶数位置代表了新链表节点
while (head != null) {
RandomListNode node = new RandomListNode(head.label);
node.next = head.next;
head.next = node;
head = node.next;
}
//修改新链表各节点的random指针指向
head = dummy.next;
while (head != null) {
if (head.random != null) {
head.next.random = head.random.next;
}
head = head.next.next;
}
//拆分原链表和新链表
head = dummy.next;
RandomListNode ans = head.next;
while (head != null) {
//记录节点
RandomListNode tmp = head.next;
//恢复指向
if (head.next != null) head.next = head.next.next;
//后移
head = tmp;
}
return ans;
}
}