【LeetCode 热题 100】19. 删除链表的倒数第 N 个结点——双指针+哨兵
Problem: 19. 删除链表的倒数第 N 个结点
题目:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度:O(L)
- 空间复杂度:O(1)
整体思路
这段代码旨在解决一个经典的链表问题:删除链表的倒数第 N 个结点 (Remove Nth Node From End of List)。问题要求在单次遍历中找到并删除链表的倒数第 n
个节点。
该算法采用了一种非常高效且经典的 双指针法(Two Pointers),结合 哨兵节点(Sentinel Node) 来巧妙地完成任务。其核心逻辑步骤如下:
-
哨兵节点 (Dummy Node):
- 算法首先创建了一个
dummy
节点,并让它的next
指针指向原始链表的头节点head
。 - 目的:这一技巧极大地简化了边界情况的处理。特别是当需要删除的节点恰好是头节点时,如果没有
dummy
节点,我们需要编写额外的逻辑来处理head
的变化。有了dummy
节点,任何节点的删除操作都统一为修改其前驱节点的next
指针,而每个节点(包括原始的头节点)都保证有一个有效的前驱节点。
- 算法首先创建了一个
-
双指针初始化与建立间距:
- 初始化两个指针
pre
和tail
,都指向dummy
节点。在这里,tail
将作为“快指针”,而pre
将作为“慢指针”。 - 通过一个
for
循环,先让快指针tail
向前移动n
步。 - 这一步完成后,
pre
和tail
之间就形成了一个固定的、包含n
个节点的“窗口”或“间距”。
- 初始化两个指针
-
双指针同步移动:
- 接下来,进入一个
while
循环,循环的条件是tail.next != null
。 - 在循环中,同时将
pre
和tail
两个指针向后移动一步。 - 由于它们之间的距离是固定的,当快指针
tail
到达链表的最后一个节点时(即tail.next
为null
),循环终止。
- 接下来,进入一个
-
定位与删除:
- 当循环终止时,由于
tail
在pre
前面n
个节点,tail
已经走到了链表末尾,那么慢指针pre
的位置就恰好是待删除节点的前一个节点。 - 执行删除操作:
pre.next = pre.next.next;
。这一行代码让pre
的next
指针“跳过”了待删除的节点,直接指向了待删除节点的下一个节点,从而有效地将其从链表中移除。
- 当循环终止时,由于
-
返回结果:
- 所有操作完成后,
dummy.next
仍然指向整个链表的(可能是新的)头节点。返回dummy.next
即可。
- 所有操作完成后,
完整代码
/*** Definition for singly-linked list.*/
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 {/*** 删除链表的倒数第 n 个结点。* @param head 链表的头节点* @param n 要删除的倒数位置* @return 删除节点后链表的头节点*/public ListNode removeNthFromEnd(ListNode head, int n) {// 创建一个哨兵节点(dummy),其 next 指向原始头节点。// 这可以统一处理删除头节点时的边界情况。ListNode dummy = new ListNode(0, head);// pre 指针最终将指向待删除节点的前一个节点。ListNode pre = dummy;// tail 指针作为快指针,用于与 pre 保持一个固定距离。ListNode tail = dummy;// 步骤 1: 让快指针 tail 先向前移动 n 步。// 这样 pre 和 tail 之间就有了 n 个节点的间距。for (int i = 0; i < n; i++) {tail = tail.next;}// 步骤 2: 同时移动 pre 和 tail 指针,直到 tail 到达链表的最后一个节点。while (null != tail.next) {tail = tail.next;pre = pre.next;}// 步骤 3: 此时 pre 正是待删除节点的前一个节点。// 执行删除操作:让 pre 的 next 指针跳过待删除节点。pre.next = pre.next.next;// 返回哨兵节点的下一个节点,即为修改后链表的头节点。return dummy.next;}
}
时空复杂度
时间复杂度:O(L)
- 指针移动:
- 第一个
for
循环,快指针tail
移动了n
步。 - 第二个
while
循环,tail
指针从第n
个节点移动到最后一个节点,移动了L - n
步(其中L
是链表的总长度)。 - 两个指针的总移动步数约为
n + (L-n) = L
。
- 第一个
- 单次遍历:整个过程相当于对链表进行了一次完整的线性扫描。
综合分析:
算法的时间复杂度与链表的长度 L
成线性关系。因此,时间复杂度为 O(L)。
空间复杂度:O(1)
- 主要存储开销:算法只创建了
dummy
,pre
,tail
等几个额外的指针变量。 - 空间大小:这些变量的数量是固定的,与输入链表的长度
L
无关。
综合分析:
算法没有使用任何与输入规模成比例的额外数据结构。因此,其额外辅助空间复杂度为 O(1)。这是一个非常高效的原地算法。