快慢指针法
1.引言
链表是一种常见的数据结构,用于存储数据元素的集合。与数组不同,链表中的每个元素都包含一个指向下一个元素的指针,这样就可以通过指针将所有元素连接起来,形成一个链式结构。
链表问题的常见性质包括:
-
- 链表的插入和删除操作通常比较高效,时间复杂度为O(1),因为只需要修改指针的指向。
-
- 链表的访问操作相对较慢,时间复杂度为O(n),因为需要从头节点开始逐个遍历到目标节点。
-
- 链表可以动态地分配内存空间,不像数组需要预先定义大小。
-
- 链表可以表示更复杂的数据结构,如栈、队列和图等。
2.快慢指针的思想
快慢指针法是一种常用的解决链表问题的算法思想。它的基本思想是使用两个指针,一个快指针和一个慢指针,它们以不同的速度遍历链表。
具体来说,快指针每次移动两步,而慢指针每次移动一步。通过这种移动方式,快指针相对于慢指针的速度是两倍。当快指针到达链表末尾时,慢指针正好指向链表的中点。
3.应用场景
-
- 寻找链表的中点:在链表中找到中点是许多问题的关键步骤。通过使用快慢指针法,快指针可以更快地遍历链表,而慢指针则以较慢的速度遍历。当快指针到达链表末尾时,慢指针正好位于链表的中间位置。
-
- 判断链表是否有环:快慢指针法也可以用于判断链表是否存在环。如果链表中存在环,那么快指针终究会追上慢指针并与其相遇。这是因为快指针每次移动两步,而慢指针每次移动一步,所以快指针会在环内多绕几圈,最终与慢指针相遇。
-
- 解决其他问题:除了寻找中点和判断是否有环外,快慢指针法还可以应用于其他链表问题,如合并两个有序链表、判断链表是否交叉等。通过调整快慢指针的移动方式,我们可以灵活地解决各种链表问题。
4.示例代码
假设我们有一个链表,其节点定义如下:
class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
现在,我们可以创建一个示例链表来演示如何使用双指针法找到链表的中点。假设链表的值为 1 -> 2 -> 3 -> 4 -> 5 -> null。
ListNode head = new ListNode(1);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(3);
ListNode node4 = new ListNode(4);
ListNode node5 = new ListNode(5);
head.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
node5.next = null;
接下来,我们使用双指针法找到链表的中点。
public static ListNode findMiddle(ListNode head) {
// 判断链表是否为空
if (head == null) {
return null;
}
// 初始化慢指针和快指针,初始位置都为头节点
ListNode slow = head;
ListNode fast = head;
// 使用快慢指针遍历链表,直到快指针到达链表末尾
while (fast.next != null && fast.next.next != null) {
// 慢指针每次移动一步
slow = slow.next;
// 快指针每次移动两步
fast = fast.next.next;
}
// 返回慢指针所指向的节点,即链表的中间节点
return slow;
}
通过上述代码,我们使用快慢指针法找到了示例链表的中点,即节点 3。
注释解释:
- 首先,我们初始化两个指针 slow 和 fast,它们都指向链表的头节点 head。
- 在循环中,每次迭代时,慢指针 slow 移动一步,而快指针 fast 移动两步。
- 循环条件是快指针 fast下一个节点 不为空且快指针的下下个节点也不为空。这是为了确保链表长度为2时,slow可以停在第一个节点以及在链表节点个数为偶数时,慢指针能够指向中点的前一个节点。
- 当循环结束时,慢指针 slow 就指向链表的中点。
需要注意的是,如果链表有偶数个节点,那么此时 slow 指针指向的是中间偏左的节点。
5.练习:可参考leetcode19题,自己练习一下快慢指针的用法。
//快慢指针法
//快指针与慢指针相距n 当快指针走到末尾时 慢指针就到了倒数第n个位置了
public static ListNode removeNthFromEnd(ListNode head, int n) {
if (head.next == null) return null; // 如果链表只有一个节点,直接返回null
ListNode dummy = new ListNode(0); // 创建虚拟头节点
dummy.next = head;
ListNode slow = dummy; // 慢指针从虚拟头节点开始
ListNode fast = dummy; // 快指针从虚拟头节点开始
int len = n; // 记录需要移除的倒数第n个节点的位置
while (len > 0) { // 快指针先向前移动n步
fast = fast.next;
--len;
}
while (fast.next != null) { // 当快指针到达链表尾部时,慢指针也到达了倒数第n个节点的前一个节点
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next; // 删除倒数第n个节点
return dummy.next; // 返回虚拟头节点的下一个节点作为新的头节点
}