链表--双指针--虚拟节点
- 力扣 142 环形链表求循环起点 重点
- 力扣 21 合并两个有序链表
- 力扣 86 分割链表
- 力扣23 合并K个有序链表 -- 优先队列(二叉堆 小顶堆)重点
- 力扣19 找倒数第N个元素 快慢指针 + 一次遍历 重点
- 力扣876 快慢指针找中间节点
- 力扣 160 相交链表 遍历“两遍”
- 辅助测试类
力扣 142 环形链表求循环起点 重点
/*力扣142 环形链表II
* 给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
* 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
* 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。
* 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
* 不允许修改 链表。*/
public static ListNode detectCycle(ListNode head) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode fast = dummy;
ListNode slow = dummy;
ListNode newHead = dummy;
boolean symbol = false;
while (fast != null && fast.next != null) {
// 注意 Fast和Fast.next 都不为 null
// 否则 Fast.next.next 会报空指针错误
fast = fast.next.next;
slow = slow.next;
if (slow == fast) {
//若能相等则 有环
// slow 走K步 fast走K步
// K = t + nS + M
// 2K = t + mS + M
// 非循环距离: t
// 循环周长 : S
// 循环起点距离相遇点: M
// 则 相遇 意味着有环 意味着 K = (m-n)S
// 则 在Fast不再动的情况下 slow 再走 K 步 依然在这个相遇点
// 则 其走K-M步一定在循环起点
// 同理 在slow走第二个K的时候同时设置一个新的指针从头结点开始也走K-M步
// 则两者应该再循环起点相遇
symbol = true;
break;
}
}
if (symbol) {
// 有环
// slow走K-M步 fast不动 newHead 走K-M步
// newHead 与 slow 在循环起点 重合
while (newHead != slow) {
newHead = newHead.next;
slow = slow.next;
}
// newHead == slow
return newHead;
} else {
// 无环
return null;
}
}
力扣 21 合并两个有序链表
/*1、合并两个有序链表
力扣 21
将两个升序链表合并为一个新的 升序 链表并返回。
新链表是通过拼接给定的两个链表的所有节点组成的
两个链表的节点数目范围是 [0, 50]
-100 <= Node.val <= 100
l1 和 l2 均按 非递减顺序 排列
*/
public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
// 设置虚拟头结点
ListNode p = dummy;
//p和dummy都是存储同一个对象地址
ListNode p1 = l1, p2 = l2;
while (p1 != null && p2 != null) {
if (p1.val < p2.val) {
p.next = p1;
p1 = p1.next;
p = p.next;
} else {
p.next = p2;
p2 = p2.next;
p = p.next;
// p要向前进
}
}
if (p1 != null) {
p.next = p1;
}
if (p2 != null) {
p.next = p2;
}
return dummy.next;
}
力扣 86 分割链表
/*
* 给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,
* 使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
* 你应当 保留 两个分区中每个节点的初始相对位置。*/
public static ListNode partition(ListNode head, int x) {
ListNode dummy1 = new ListNode(-1);
ListNode dummy2 = new ListNode(-1);
ListNode h = head;
ListNode p1 = dummy1;
ListNode p2 = dummy2;
while (h != null) {
if (h.val < x) {
p1.next = h;
// 向dummy1 的链表尾部添加 h节点
h = h.next;
// head 链表向后走一步
p1 = p1.next;
// p1 指向 dummy1链表末尾节点
p1.next = null;
// p1 末尾的next 设置为 null
} else {
p2.next = h;
h = h.next;
p2 = p2.next;
p2.next = null;
}
}
p1.next = dummy2.next;
return dummy1.next;
}
力扣23 合并K个有序链表 – 优先队列(二叉堆 小顶堆)重点
/*给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。*/
public static ListNode mergeKLists(ListNode[] lists) {
if (lists.length < 1) {
// 如果用 < 1 的值创建优先队列 则会产生异常
// IllegalArgumentException - if initialCapacity is less than 1
return null;
} else {
PriorityQueue<ListNode> priorityQueue = new PriorityQueue<ListNode>(lists.length, new Comparator<ListNode>() {
@Override
public int compare(ListNode o1, ListNode o2) {
return o1.val - o2.val;
}
});
// 优先级队列(二叉堆) 设置为最小堆 队列长度为lists的元素个数 有K个链表 传入 K 为队列长度
ListNode dummy = new ListNode(-1); // 设置虚拟头结点 等待最后返回dummy.next
ListNode p = dummy;
for (ListNode listNode : lists) {
if (listNode != null) {
priorityQueue.add(listNode);
// 如果 此链表不为空 则将头结点 存入最小堆的优先队列
}
}
while (!priorityQueue.isEmpty()) {
// 当优先队列内元素个数不为空 时
ListNode temp = priorityQueue.poll();// temp 指向 队列头元素 同时队头出队
p.next = temp;
p = p.next;
if (temp.next != null) {
priorityQueue.add(temp.next); // 刚出队的链表的下一个元素 进入队列
}
}
return dummy.next;
}
}
力扣19 找倒数第N个元素 快慢指针 + 一次遍历 重点
/*力扣19 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
* 要求: 只遍历一次结点 思路:两个指针一个在前一个在后
* 因为要往前走N步 设置虚拟头结点的情况下 代码更清楚简洁 不必要多判断
* 只有一个元素的情况(因为如果不用虚拟头结点的话, 只有一个元素时起始节点就在此)*/
public static ListNode removeNthFromEnd(ListNode head, int n) {
// 题目默认head不为空 链表元素个数>=1
// 先要找到倒数 第N个节点
ListNode dummy = new ListNode(-1);
// 设置虚拟头结点 方便向前走N步
dummy.next = head;
ListNode p1 = dummy;
ListNode p2 = dummy;
for (int i = 0; i < n; i++) {
p1 = p1.next;
// 向前走N步
}
if (p1 == null) {
return null;
// 链表不够长 总数都不够N
}
while (p1.next != null) {
p1 = p1.next;
p2 = p2.next;
}
// 当 p1 为 末尾元素时 退出循环
// 此时 p2.next 指向 倒数第N个元素
ListNode p3 = p2.next.next;
p2.next = p3;
// 删除倒数第N个元素
return dummy.next;
}
力扣876 快慢指针找中间节点
/*力扣876
给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
思路: 虚拟节点 + 快慢指针 快的一次走两步 慢的一次走一步
当快的指向空的时候 慢的刚好在中间节点的位置(同时适用于奇数个元素和偶数个元素的情况)*/
public static ListNode middleNode(ListNode head) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode p1 = dummy;
ListNode p2 = dummy;
while (p1 != null) {
p2 = p2.next;
// 奇数个元素时 最后一步走之前 p1.next != null && p1.next.next == null 成立
// 偶数个元素时 最后一步走之前 p1.next == null 成立
// 所以 让p1=p1.next.next 为一大步 的情况下
// 偶数个的情况下不能走最后一大步所以下面用了break
// 但P2 必须要走这一步
if (p1.next == null) {
break;
} else {
p1 = p1.next.next;
}
}
return p2;
}
力扣 160 相交链表 遍历“两遍”
/*
* 力扣160 相交链表
* 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。
* 如果两个链表不存在相交节点,返回 null 。
* 思路:两个指针同步向前走 A走到末尾之后下一步走B的头部,B走到末尾之后下一步走到A的头部
* 则 两个指针会同时在第一个合并节点处同步到达 p==q 退出循环
* 如果没有共同部分 则 会同时到null 依然满足p==q 退出循环 */
public static ListNode getIntersectionNode(ListNode headA, ListNode headB) {
/*
很奇怪 我用虚拟头结点 就超时 放弃用虚拟头结点就通过力扣测试
// 若用虚拟头结点 则要大量使用 p.next 而我们要 在末尾 重新转入 另一个链表表头
// 故在转入的时候 不要用next 防止篡改 链表 导致死循环
ListNode dummy = new ListNode(-1);
dummy.next = headA;
ListNode dummy2 = new ListNode(-1);
dummy2.next = headB;
ListNode p = dummy;
ListNode q = dummy2;
while (p != q) {
if (p.next == null) {
// 将P的位置改变 而不改变原链表结构
p = headB;
}
if (q.next == null) {
// 将Q的位置改变 而不改变原链表结构
q = headA;
}
p = p.next;
q = q.next;
}
return p;*/
ListNode p = headA;
ListNode q = headB;
while (p != q) {
if (p == null) {
// 将P的位置改变 而不改变原链表结构
p = headB;
} else {
p = p.next;
}
if (q == null) {
// 将Q的位置改变 而不改变原链表结构
q = headA;
} else {
q = q.next;
}
}
return p;
}
辅助测试类
package com.caoii.LinkedList;
public class ListNode {
public int val;
public ListNode next;
ListNode() {
}
public ListNode(int val) {
this.val = val;
}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
package com.caoii;/*
*@program:labu-pratice-study
*@package:com.caoii
*@author: Alan
*@Time: 2024/4/14 11:38
*@description: 双指针解决链表问题相关应用的测试
*/
import com.caoii.LinkedList.DoublePointerInLinkedList;
import com.caoii.LinkedList.ListNode;
import org.junit.jupiter.api.Test;
public class DoublePointerTest {
/*力扣21 有序链表合并*/
@Test
public void test_01() {
ListNode l1 = new ListNode(1);
ListNode p1 = l1;
p1.next = new ListNode(2);
p1 = p1.next;
p1.next = new ListNode(4);
ListNode l2 = new ListNode(1);
ListNode p2 = l2;
p2.next = new ListNode(3);
p2 = p2.next;
p2.next = new ListNode(9);
ListNode returnListNode = DoublePointerInLinkedList.mergeTwoLists(l1, l2);
ListNode p = returnListNode;
while (p != null) {
System.out.print(p.val + " ");
p = p.next;
}
System.out.println();
}
/*力扣86 将一个链表分割成两个链表再合并*/
@Test
public void test_02() {
ListNode l1 = new ListNode(1);
ListNode p1 = l1;
p1.next = new ListNode(4);
p1 = p1.next;
p1.next = new ListNode(3);
p1 = p1.next;
p1.next = new ListNode(2);
p1 = p1.next;
p1.next = new ListNode(5);
p1 = p1.next;
p1.next = new ListNode(2);
ListNode returnListNode = DoublePointerInLinkedList.partition(l1, 3);
ListNode p = returnListNode;
while (p != null) {
System.out.print(p.val + " ");
p = p.next;
}
System.out.println();
}
/*力扣 23 合并K个有序链表*/
@Test
public void test_03() {
ListNode l1 = new ListNode(1);
ListNode p1 = l1;
p1.next = new ListNode(4);
p1 = p1.next;
p1.next = new ListNode(5);
ListNode l2 = new ListNode(1);
ListNode p2 = l2;
p2.next = new ListNode(3);
p2 = p2.next;
p2.next = new ListNode(4);
ListNode l3 = new ListNode(2);
ListNode p3 = l3;
p3.next = new ListNode(6);
ListNode[] lists = new ListNode[]{
l1, l2, l3
};
ListNode returnListNode = DoublePointerInLinkedList.mergeKLists(lists);
ListNode p = returnListNode;
while (p != null) {
System.out.print(p.val + " ");
p = p.next;
}
System.out.println();
}
/*力扣19 一次遍历 删除倒数第N个元素 */
@Test
public void test_04() {
ListNode l1 = new ListNode(1);
ListNode p1 = l1;
p1.next = new ListNode(2);
p1 = p1.next;
p1.next = new ListNode(3);
p1 = p1.next;
p1.next = new ListNode(4);
p1 = p1.next;
p1.next = new ListNode(5);
ListNode returnListNode = DoublePointerInLinkedList.removeNthFromEnd(l1, 2);
ListNode p = returnListNode;
while (p != null) {
System.out.print(p.val + " ");
p = p.next;
}
System.out.println();
}
/*力扣876 找链表的中间节点 奇数个找中间 偶数个找中间两个的后一个 */
@Test
public void test_05() {
ListNode l1 = new ListNode(1);
ListNode p1 = l1;
p1.next = new ListNode(2);
p1 = p1.next;
p1.next = new ListNode(3);
p1 = p1.next;
p1.next = new ListNode(4);
p1 = p1.next;
p1.next = new ListNode(5);
p1 = p1.next;
p1.next = new ListNode(6);
ListNode returnListNode = DoublePointerInLinkedList.middleNode(l1);
ListNode p = returnListNode;
// 输出从中间节点开始的后半截
while (p != null) {
System.out.print(p.val + " ");
p = p.next;
}
System.out.println();
}
/*力扣142 环形链表求循环起点 */
@Test
public void test_06() {
ListNode l1 = new ListNode(3);
ListNode p1 = l1;
p1.next = new ListNode(2);
p1 = p1.next;
ListNode temp = p1;
// temp 辅助形成循环链表
p1.next = new ListNode(0);
p1 = p1.next;
p1.next = new ListNode(-4);
p1 = p1.next;
p1.next = temp;
// 形成循环链表
ListNode returnListNode = DoublePointerInLinkedList.detectCycle(l1);
ListNode p = returnListNode;
System.out.println("循环起点的值:" + p.val);
}
/*力扣160 相交链表求第一个交点 */
@Test
public void test_07() {
ListNode l1 = new ListNode(-3);
ListNode l2 = new ListNode(30);
ListNode p1 = l1;
ListNode p2 = l2;
ListNode temp = new ListNode(300);
ListNode p3 = temp;
p3.next = new ListNode(400);
p3 = p3.next;
p3.next = new ListNode(500);
p3 = p3.next;
p1.next = new ListNode(-4);
p1 = p1.next;
p1.next = new ListNode(-5);
p1 = p1.next;
p1.next = temp;
p2.next = new ListNode(40);
p2 = p2.next;
p2.next = new ListNode(50);
p2 = p2.next;
p2.next = new ListNode(60);
p2 = p2.next;
p2.next = temp;
ListNode returnListNode = DoublePointerInLinkedList.getIntersectionNode(l1, l2);
ListNode p = returnListNode;
System.out.println("第一个交点的值:" + p.val);
}
}