1. 相交链表
当一个链表到头后,交换另一个链表,继续,直到2个节点相等
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pa = headA;
ListNode pb = headB;
while (pa != pb) {
if (pa != null) {
pa = pa.next;
} else {
pa = headB;
}
if (pb != null) {
pb = pb.next;
} else {
pb = headA;
}
}
return pa;
}
2.反转链表
1.迭代法
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
2.递归
public ListNode reverseList(ListNode head) {
if ((head == null) || (head.next == null)) {
return head;
}
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
3.回文链表
找到中间节点,然后反转中间节点的后续节点,然后再2部分比较
注意:
1. 找到中间结点,其实对应2种方式,这2种方式找到的结点,略微有点差异。如果不注意会出现空指针异常。主要是因为 反转链表时修改了结点的next指针,中间结点指向null了。
2. 反转链表时会修改结点的next指向,导致链表从中点断开,两边链表长度不一致时同时遍历容易出现空指针问题。
private void testMidReversList(int[] arr) {
//arr=[1->2->3->4]
ListNode head = createLinkListInsertTail(arr);
traverse(head, "create head"); //create head=[1->2->3->4]
ListNode mid = findMidNodeFromHeadNext(head);
traverse(mid, "mid"); //mid=[2->3->4]
ListNode reverse = reverseList(mid); //反转时会从中点断开链表
traverse(reverse, "reverse"); //reverse=[4->3->2]
ListNode left = head;
ListNode right = reverse;
traverse(left, "left"); //left=[1->2] 已经断开了,head next指针被修改
traverse(right, "right"); //right=[4->3->2]
}
第一种 fast =head
public ListNode findMidNodeFromHead(ListNode head) {
//初始化是slow和fast都指向 head
ListNode slow = head;
ListNode fast = head;
while ((fast != null) && (fast.next != null)) {
slow = slow.next;
fast = fast.next.next;
}
//这种找到的中间结点,在偶数时会有差异,由于fast只比slow快一步,所以中点找到的是偶数中间2个点的最后一个
//2.不管那种方式当链表长度为奇数时,找到的中点都是一个
return slow;
}
由于找到的中点位置的不同,所以在遍历时也会有略微的不同
public boolean isPalindrome(ListNode head) {
if ((head == null) || (head.next == null)) {
return true;
}
ListNode slow = head;
ListNode fast = head;
while ((fast != null) && (fast.next != null)) {
slow = slow.next;
fast = fast.next.next;
}
ListNode reverse = reverse(slow);
ListNode left = head;
ListNode right = reverse;
//由于这种方式找的mid靠后,所以right会相对较少,使用right遍历
while (right != null) {
if (left.val != right.val) {
return false;
}
left = left.next;
right = right.next;
}
return true;
}
public ListNode reverse(ListNode node) {
ListNode prev = null;
ListNode curr = node;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
第二种 fast = head.next;
public ListNode findMidNodeFromHeadNext(ListNode head) {
//初始化是slow和fast时,fast已经比slow 快一步了
ListNode slow = head;
ListNode fast = head.next;
while ((fast != null) && (fast.next != null)) {
slow = slow.next;
fast = fast.next.next;
}
//1.这种找到的中间结点,在偶数时会有差异,由于fast只比slow快2步,所以中点找到的是偶数中间2个点的最前面一个
//2.不管那种方式当链表长度为奇数时,找到的中点都是一个
return slow;
}
public boolean isPalindrome(ListNode head) {
//没有结点或者只有1个结点的时候,也是 回文链表
if ((head == null) || (head.next == null)) {
return true;
}
ListNode mid = findMidNodeFromeHeadNext(head);
ListNode reverse = reverseFromRecursion(mid);
ListNode left = head;
ListNode right = reverse;
while (left != null) { //由于使用的时head next找中点,所以使用left
if (left.val != right.val) {
return false;
}
left = left.next;
right = right.next;
}
return true;
}
// 使用 fast = head; 找中点
private ListNode findMidNodeFromHead(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while ((fast != null) && (fast.next != null)) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// 使用 fast = head.next; 找中点
private ListNode findMidNodeFromeHeadNext(ListNode head) {
ListNode slow = head;
ListNode fast = head.next;
while ((fast != null) && (fast.next != null)) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
private ListNode reverseFromIterator(ListNode node) {
ListNode prev = null;
ListNode curr = node;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
private ListNode reverseFromRecursion(ListNode node) {
if ((node == null) || (node.next == null)) {
return node;
}
ListNode newHead = reverseFromRecursion(node.next);
node.next.next = node;
node.next = null;
return newHead;
}
4.环形链表
给你一个链表的头节点 head
,判断链表中是否有环。
当2个节点相等时,需要退出循环
思路:使用“快慢指针”法来判断链表是否有环
使用两个指针,一个快指针(每次移动两步)和一个慢指针(每次移动一步)。如果链表中存在环,快指针和慢指针最终会相遇;如果没有环,快指针会到达 null
。
public boolean hasCycle(ListNode head) {
if ((head == null) || (head.next == null)) {
return false;
}
ListNode slow = head;
ListNode fast = head;
while ((fast != null) && (fast.next != null)) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) { // 环形链表,慢指针和快指针相遇
return true;
}
}
return false;
}
5.环形链表II
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
核心思想:
1. 先找到2个节点相遇的时候
2. 相遇时slow节点移回到head节点
3. slow和fast都已一步的前进,再次相遇就是环的入口
public ListNode detectCycle(ListNode head) {
if ((head == null) || (head.next == null)) {
return null;
}
ListNode slow = head;
ListNode fast = head;
// 1. 判断链表是否有环
while ((fast != null) && (fast.next != null)) {
slow = slow.next;
fast = fast.next.next;
// 2. slow 和 fast 相遇说明有环
if (slow == fast) {
slow = head; // 慢指针回到链表头
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow; // slow 和 fast 相遇的地方就是环的入口节点
}
}
// 如果没有环
return null;
}
延伸扩展 环的大小
- 判断链表是否有环:首先用快慢指针找出链表中的环,确认链表有环。
- 计算环的大小:如果找到相遇点,将
current
指针固定在该相遇点,沿环前进并计数,直到回到起始点。每前进一步计数加一,最终得到环的长度。
public int getCycleLength(ListNode head) {
if ((head == null) || (head.next == null)) {
return 0;
}
ListNode slow = head;
ListNode fast = head;
// 1. 判断链表是否有环并找到相遇点
while ((fast != null) && (fast.next != null)) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) { // 找到环
// 2. 计算环的大小
int length = 1;
ListNode curr = slow;
while (curr.next != slow) {
length++;
curr = curr.next;
}
return length;
}
}
// 没有环
return 0;
}
环形链表的几个问题本质上都是需要先确定是否存在环,存在并且找到环的相遇点,然后再此基础上进一步的操作
6.合并2个有序链表
记得链表剩余时,直接添加到末尾
方法一:迭代法
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while ((list1 != null) && (list2 != null)) {
if (list1.val <= list2.val) {
curr.next = list1;
list1 = list1.next;
} else {
curr.next = list2;
list2 = list2.next;
}
curr = curr.next;
}
// 接上剩余节点
curr.next = (list1 != null) ? list1 : list2;
return dummy.next;
}
方法二:递归法
- 比较
l1
和l2
的当前节点值。 - 如果
l1
的值较小,将l1.next
指向合并l1.next
和l2
的结果。 - 如果
l2
的值较小,将l2.next
指向合并l1
和l2.next
的结果。 - 返回较小的节点作为新的头节点。
public ListNode mergeTwoListsRecursive(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val < l2.val) {
l1.next = mergeTwoListsRecursive(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoListsRecursive(l1, l2.next);
return l2;
}
}
- 迭代法:时间复杂度为 O(m + n),其中
m
和n
是两个链表的长度。空间复杂度为 O(1),因为我们只用了一些辅助指针。 - 递归法:时间复杂度也为 O(m + n),但空间复杂度为 O(m + n),因为递归会使用额外的栈空间。
7. 两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
最主要是需要考虑进位
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 创建一个哑节点(dummy),方便处理头节点的特殊情况
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
int carry = 0;
// 遍历链表,直到 l1 和 l2 都为空,且没有进位
while ((l1 != null) || (l2 != null) || (carry != 0)) {
int sum = carry; // 从进位开始
if (l1 != null) {
sum = sum + l1.val;
l1 = l1.next;
}
if (l2 != null) {
sum = sum + l2.val;
l2 = l2.next;
}
// 更新进位和当前节点的值
carry = sum / 10; // 进位值
curr.next = new ListNode(sum % 10); // 新建节点存储个位数
curr = curr.next; // 后移当前指针
}
return dummy.next; // 返回哑节点后的链表头
}
8.删除链表的倒数第N个结点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
使用dummy节点主要是为了方便操纵删除头节点的场景
删除结点的语句:curr.next = curr.next.next;
为什么需要fast先移动N+1步的原因
fast
指针先移动 n+1
步的原因是为了让 slow
指针停在要删除节点的前一个节点位置。这样我们可以直接让 slow.next
跳过要删除的节点,实现删除操作。
假设链表有 L
个节点,要删除倒数第 n
个节点:
-
倒数第 n 个节点的前一个节点:在删除操作中,我们需要让
slow
指针停在倒数第n
个节点的前一个节点,才能直接修改slow.next
指针跳过目标节点。 -
双指针距离控制:通过让
fast
先移动n+1
步,可以使fast
和slow
指针之间保持n
个节点的间隔。当fast
移动到链表末尾(即null
)时,slow
指针正好到达倒数第n
个节点的前一个节点。 -
删除节点:此时,
slow.next
就是我们要删除的节点。通过让slow.next = slow.next.next
,可以跳过目标节点并完成删除。
public ListNode removeNthFromEnd(ListNode head, int n) {
//使用 dummy 节点可以统一处理删除头节点的情况,不需要额外判断
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode slow = dummy;
ListNode fast = dummy;
// 1. 让 fast 指针先移动 n+1 步
for (int i = 0; i <= n; i++) {
fast = fast.next;
}
// 2. 同时移动 fast 和 slow,直到 fast 到达链表末尾
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
// 3. 删除 slow 的下一个节点
slow.next = slow.next.next;
// 返回头节点
return dummy.next;
}
9.两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)
public ListNode swapPairs(ListNode head) {
if ((head == null) || (head.next == null)) {
return head;
}
ListNode dummy = new ListNode(0);
dummy.next = head;
// 一组反转节点的前一个位置,初始化为dummy
ListNode prevGroup = dummy;
// 判断是否存在要反转的节点
while ((prevGroup.next != null) && (prevGroup.next.next != null)) {
// 取2个待反转的节点,方便后续操作
ListNode first = prevGroup.next;
ListNode second = prevGroup.next.next;
//更新一组反转节点的指向,可以理解成连接反转后的节点,
//即一组的前一个结点要指向反转后的第一个结点
prevGroup.next = second;
//第一个节点指向第二个节点的下一个节点,修改第一个节点的指向
//即第一个结点要指向第二个结点的下一个结点,保证不断链
first.next = second.next;
//修改第二个节点的指向,实现节点的反转,
//即第二个结点要指向第一个结点,实现反转
second.next = first;
//更新一组反转节点的位置,移动到反转结束后的位置,即时第一个节点的位置
//即反转完一组后,移动到下一组反转的前面,准备下一轮反转
prevGroup = first;
}
return dummy.next;
}
10. K个一组反转链表
public ListNode reverseKGroup(ListNode head, int k) {
if ((head == null) || (head.next == null) || (k <=1)) {
return head;
}
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prevGroupKEnd = dummy;
ListNode curr = head;
while (curr != null) {
//需要反转的一组节点的开始节点
ListNode startGroup = curr;
int i = 0;
//curr是用来遍历整个链表的,也是每次取K个节点进行反转的关键,找到下一组需要反转的开始节点
//不过需要注意的是,当节点不足K个时,需要对节点进行判空,否则容易空指针异常
for (; (i<k) && (curr != null); i++) {
curr = curr.next;
}
if (i == k) {
//当满足一组节点时,修改一组节点的指向,连接链表,同时更新一组节点尾的位置
prevGroupKEnd.next = reverseK(startGroup, k);
prevGroupKEnd = startGroup;
} else {
//不满足时直接修改一组节点尾指向下一组节点的开始节点
prevGroupKEnd.next = startGroup;
}
}
return dummy.next;
}
private ListNode reverseK(ListNode node, int k) {
ListNode prev = null;
ListNode curr = node;
for (int i=0; i<k; i++) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
11.随机链表的复制
给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next
指针和 random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。返回复制链表的头节点。
public Node copyRandomList(Node head) {
if (head == null) {
return head;
}
Map<Node, Node> nodes = new HashMap<>();
Node curr = head;
//遍历一遍节点,保存到map中,key=当前的节点,value=copy节点,
//只有值,next和random没有复制
while (curr != null) {
nodes.put(curr, new Node(curr.val));
curr = curr.next;
}
curr = head;
// 从map里面分别对copy节点进行next和random结点赋值
while (curr != null) {
Node copyNode = nodes.get(curr);
copyNode.next = nodes.get(curr.next);
copyNode.random = nodes.get(curr.random);
curr = curr.next;
}
return nodes.get(head);
}
12.排序链表
给你链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
找结点的中点,断开时容易犯的错误
head=[4 -> 2 -> 1 -> 3]
{
ListNode slow = head;
ListNode fast = head;
while ((fast != null) && (fast.next != null)) {
slow = slow.next;
fast = fast.next.next;
}
分割完后
left = sortList(head):此时 head 仍然是指向链表的头节点 4,链表为 4 -> 2 -> null。
现在问题出在 left = sortList(head) 这一步:
head 指向的是 4 -> 2 -> null,但链表已经被从 2 断开,因此此时递归处理的左半部分链表没有减少长度。
当你递归处理 4 -> 2 -> null 时,慢指针会再次指向 2,然后断开,
--------------------------------------------------------------------
这样会导致递归始终无法退出,栈溢出
正确找到中点并且断开的
ListNode slow = head;
ListNode fast = head.next; // 快指针从 head.next 开始,确保分割准确
// 快慢指针移动,找到中点
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// mid 是后半部分的开头
ListNode mid = slow.next;
slow.next = null; // 断开链表
方法:归并排序
归并排序的步骤如下:
- 找到链表的中点:使用快慢指针找到链表的中点,将链表分为两半。
- 递归地对两半链表进行排序:递归地将每一半链表拆分并排序。
- 合并两个已排序的链表:用一个辅助函数来合并两个有序链表。
public ListNode sortList(ListNode head) {
// 基本情况:空链表或只有一个节点
if ((head == null) || (head.next == null)) {
return head;
}
// 1. 使用快慢指针找到链表的中点,使用的是靠前中点,
// 这种方式当有偶数个结点时能较好的平分2个链表
ListNode mid = findMidNodeFromHeadNext(head);
ListNode rightHead = mid.next; // 右边链表的第一个结点
mid.next = null; // 将链表分成两半
// 2. 递归排序左右两半
ListNode left = sortList(head);
ListNode right = sortList(rightHead);
// 3. 合并排序后的两半
return mergeTwoLists(left, right);
}
private ListNode findMidNodeFromHeadNext(ListNode head) {
//初始化是slow和fast时,fast已经比slow 快一步了
ListNode slow = head;
ListNode fast = head.next;
while ((fast != null) && (fast.next != null)) {
slow = slow.next;
fast = fast.next.next;
}
//1.这种找到的中间结点,在偶数时会有差异,
//由于fast只比slow快2步,所以中点找到的是偶数中间2个点的最前面一个
//2.不管那种方式当链表长度为奇数时,找到的中点都是一个
return slow;
}
private ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while ((list1 != null) && (list2 != null)) {
if (list1.val <= list2.val) {
curr.next = list1;
list1 = list1.next;
} else {
curr.next = list2;
list2 = list2.next;
}
curr = curr.next;
}
// 接上剩余节点
curr.next = (list1 != null) ? list1 : list2;
return dummy.next;
}