1. 方法论
链表解题方法论:
- 对于笔试:一切为了时间复杂度,不用在乎空间复杂度
- 对于面试:时间复杂度放在首位,空间复杂度尽量最小
重要技巧
- 额外数据结构记录(哈希表等)
- 快慢指针
2. 链表回文
方式1 利用快慢指针走到中点,将链表后一半放入栈
public boolean isPalindrome(Node head) {
// 链表为空或者只有单个结点时,返回true
if (head == null || head.next == null) {
return true;
}
// 定义右指针
Node right = head.next;
// 定义当前指针
Node cur = head;
while (cur.next != null && cur.next.next != null) {
// 慢指针每次走一步
right = right.next;
// 快指针每次走两步
cur = cur.next.next;
}
// 将链表的后半部分装入栈中
while (right != null) {
stack.push(right);
right = right.next;
}
while (!stack.isEmpty()) {
if (head.value != satck.pop().value) {
return false;
}
head = head.next;
}
return true;
}
方式2 利用快慢指针走到中点,将链表后一部分反转,双指针遍历
public static boolean isPalidrome(Node head) {
// 链表为空或者只有单个结点时,返回true
if (head == null || head.next == null) {
return true;
}
// 定义慢指针n1
Node n1 = head;
// 定义快指针n2
Node n2 = head;
while (n2.next != null && n2.next.next != null) {
// 慢指针每次走一步
n1 = n1.next;
// 快指针每次走两步
n2 = n2.next.next;
// 当快指针走到链表末尾,慢指针便到达链表中点
}
// n2此时指向右半部分链表的第一个结点
n2 = n1.next;
// 令中点的后继为空,断链
n1.next = null;
// 定义n3 辅助转置
Node n3 = null;
// 右半部分链表转置
while (n2 != null) {
// n3用于保存后继结点
n3 = n2.next;
// 转置操作
n2.next = n1;
// n1指向n2
n1 = n2;
// n2指向n3
n2 = n3;
}
// n3指向转置后的链表的(从左至右)最后一个结点
n3 = n1;
// n2指向转置后的链表的(从左至右)第一个结点
n2 = head;
boolean res = true;
// 检查回文
while (n1 != null && n2 != null) {
if (n1.value != n2.value) {
res = false;
break;
}
// 从左到中点
n1 = n1.next;
// 从右到中点
n2 = n2.next;
}
n1 = n3.next;
n3.next = null;
// 复原链表
while (n1 != null) {
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
3. 链表划分
将单向链表按某值划分成左边小、中间相等、右边大的形式
[题目] 给定一个单链表的头节点head,节点的值类型是整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于pivot的节点,中间部分都是值等于pivot的节点,右部分都是值大于pivot的节点。
[进阶] 在实现原问题功能的基础上增加如下的要求
- 调整后所有小于pivot的节点之间的相对顺序和调整前一样
- 调整后所有等于pivot的节点之间的相对顺序和调整前一样
- 调整后所有大于pivot的节点之间的相对顺序和调整前一样
- 时间复杂度请达到O(N),额外空间复杂度请达到o(1)。
[参考图解]
public Node listPartition(Node head, int pivot) {
// 定义sH、sT(小于pivot的部分)
Node sH = null; Node sT = null;
// 定义eH、eT(等于pivot的部分)
Node eH = null; Node eT = null;
// 定义bH、bT(大于pivot的部分)
Node bH = null; Node bT = null;
Node next = null;
while (head != null) {
// 断链
next = head.next;
head.next = null;
// 如果当前值小于枢轴
if (head.value < pivot) {
// 如果左半段部分没有任何结点时
// s头指针sH和s尾指针sT都指向head
if (sH == null) {
sH = head;
sT = head;
} else {
// s尾指针sT连接当前节点
sT.next = head;
// 更新s尾指针sT
sT = head;
}
} else if (head.value == pivot) { // 如果当前值等于枢轴
// 如果中间段部分没有任何结点时
// e头指针eH和e尾指针eT都指向head
if (eH == null) {
eH = head;
eT = head;
} else {
// e尾指针eT连接当前节点
eT.next = head;
// 更新e尾指针eT
eT = head;
}
} else { // 如果当前值大于枢轴
// 如果右半部分没有任何结点时
// b头指针bH和b尾指针bT都指向head
if (bH == null) {
bH = head;
bT = head;
} else {
// b尾指针bT连接当前节点
bT.next = head;
// 更新b尾指针eT
bT = head;
}
}
// 更新当前节点位置
head = next;
}
// 如果存在左半段链表[详解图解]
if (sT != null) {
// s尾指针sT后继指向e头指针eH
sT.next = eH;
eT = eT == null ? sT : eT;
}
// 如果左半段链表和中间段链表不是都没有[详解图解]
if (eT != null){
eT.next = bH;
}
return sH != null ? sH : eH != null ? eH : bH;
}
[此处图解,后续补充,近期较忙]
4. 复制含有随机指针结点的链表
[题目] 定义一种特殊单链表结点类,描述如下:
class Node {
int value;
Node next;
Node rand;
Node(int val) {
value = val;
}
}
rand指针是单链表结点结构中新增的指针,rand可能指向链表中的任意一个结点,也可能指向null。给定一个由Node结点类型组成的无环单链表的头结点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头结点。
[要求] 时间复杂度O(N),额外空间复杂度O(1)
方式1 哈希表
[参考图解]
public Node copyListWithRandom(Node head) {
// 构建原始结点映射至克隆新结点的哈希表
HashMap<Node, Node> map = new HashMap<>();
Node cur = head;
// 拷贝原始链表
while (cur != null) {
map.put(cur, new Node(cur.value));
cur = cur.next;
}
cur = head;
while (cur != null) {
// 拷贝结点的next域指向原始结点next域对应的拷贝结点
map.get(cur).next = map.get(cur.next);
// 拷贝结点的rand域指向原始结点rand域对应的拷贝结点
map.get(cur).rand = map.get(cur.rand);
cur = cur.next;
}
return map.get(head);
}
5.链表相交问题
[题目] 给定两个可能有环也可能无环的单链表,头结点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个结点。如果不相交,返回null。
[要求] 如果两个链表长度和为N,时间复杂度达到O(N),额外空间复杂度达到O(1)
5.1 单个链表有环问题
[Q1] 如何判断一个链表有环,如果有,返回第一个入环口,没有返回null
算法思想
- 设置快慢指针,快指针走两步,慢指针走一步
- 如果没有环快指针会先到达终点
- 如果有环,快指针与慢指针会在环中某一位置相
- 当快慢指针相遇时,快指针指向head,与慢指针同步移动,会在第一个入环口相遇。
public Node getLoopNode(Node head) {
// 头结点为空或者只有单个头结点时返回null
if (head == null || head.next == null) {
return null;
}
// 定义慢指针
Node slow = head.next;
// 定义快指针
Node fast = head.next.next;
while (fast != slow) {
if (fast.next == null || fast.next.next == null) {
return null;
}
fast = fast.next.next;
slow = slow.next;
}
// 此时fast指向head
fast = head;
// 此时slow与fast同步移动
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
注
解决了问题1就知道两个链表有环或者无环情况。如果一个链表有环,另一个链表无环,那么这个链表是无论也不可能相交的。能相交的就是两个情况:
- 两个链表都无环(Q2)
- 两个链表都有环(Q3)
5.2 两个无环链表相交问题
[Q2] 如何判断两个无环链表是否相交,相交则返回第一个相交节点,不相交返回null
算法思想
- a. 指针1从头结点开始,走到最后一个结点(不是结束),统计链表1的长度len1,记录链表1的最后一个结点为end1
- b. 链表2从头结点开始,走到最后一个结点(不是结束),统计链表2的长度len2,记录链表2的最后给一个结点end2
- c. 如果end1!=end2,说明两个链表不相交,返回null即可;如果end1==end2,说明两个链表相交,进入步骤d. 来寻找第一个相交的结点
- d. 如果链表1比较长,链表1就先走len1-len2步;如果链表2比较长,链表2就先走len2-len1步。然后两个链表一起走,两个链表第一次走到一起的结点就是第一个相交的结点
public Node noLoop(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node cur1 = null;
Node cur2 = null;
int len = 0;
// 求链表1的长度
while (cur1.next != null) {
len++;
cur1 = cur1.next;
}
// 求链表1与链表2的长度差
while (cur2.next != null) {
len--;
cur2 = cur2.next;
}
if (cur1 != cur2) {
return null;
}
// cur1选择长的链表
cur1 = n > 0 ? head1 : head2;
// cur2选择短的链表
cur2 = cur1== head1 ? head2 : head1;
len = Math.abs(len);
while (len != 0) {
len--;
cur1 = cur1.next;
}
// 两指针同步移动
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
5.3 两个有环链表相交问题
[Q3] 如何判断两个有环链表相交,相交则返回第一个相交节点,不相交返回null
[loop1 == loop2 两个链表相交图示]
[loop1 != loop2 两个链表不相交图示]
[loop1 != loop2两个链表相交图示]
算法思想
- 让链表1从入环扣1(loop1)出发(由于loop1和之后的所有结点都在环熵,将来一定能返回loop1)
- 如果回到loop1之前没有遇到loop2,说明两个链表结构如图
[loop1 != loop2 两个链表不相交图示]
,即不相交,返回null - 如果回到loop1之前遇到loop2,说明两个链表结构如图
[loop1 != loop2两个链表相交图示]
,也就是相交 - 由于loop1和loop2都在两天链表上,只不过loop1是离链表1较近的结点,loop2是离链表2较近的结点,返回两者中任意一个即可
public Node bothLoop(Node head1, Node head2, Node loop1, Node loop2){
Node cur1 = null;
Node cur2 = null;
// 图示1
if (loop1 == loop2) {
cur1 = head1;
cur2 = head2;
int len = 0;
while (cur1 != loop1) {
len++;
cur = cur1.next;
}
while (cur2 != loop2) {
len--;
cur2 = cur1.next;
}
cur1 = len > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
len = Math.abs(len);
while (len != 0) {
len--;
cur1 = cur.next;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
} else { // 图示2 3
cur1 = loop1.next;
while (cur1 != loop1) {
if (cur1 == loop2) {
return loop1;
}
cur1 = cur1.next;
}
return null;
}
}
[综上]
public class Node {
public int value;
public Node next;
public Node(int value) {
this.value = value;
}
}
public Node getIntersectNode(Node head1, Node head2) {
// 两个链表头结点为空 直接返回null
if (head1 == null || head2 == null) {
return null;
}
// 判断第一个链表有没有环
Node loop1 = getLoopNode(head1);
// 判断第一个链表有没有环
Node loop2 = getLoopNode(head2);
// 情况1
if (loop1 == null && loop2 == null) {
return noLoop(head1, head2);
}
// 情况2/3
if (loop1 != null && loop2 != null) {
return bothLoop(head1, loop1, head2, loop2);
}
return null;
}
待更新~