题目信息
- 题目编号: 19
- 题目名称: 删除链表的倒数第N个节点
- 标签: 链表、双指针
- 难度: 中等
- 题目链接 : LeetCode 链接
题目描述
给定一个链表的头节点 head,删除该链表中倒数第 n 个节点,并返回头节点。
示例
示例 1:
输入: head = [1,2,3,4,5], n = 2
输出: [1,2,3,5]
解释: 链表为 1->2->3->4->5,删除倒数第2个节点(值为4),结果为 1->2->3->5
示例 2:
输入: head = [1], n = 1
输出: []
解释: 链表只有一个节点,删除后为空链表
示例 3:
输入: head = [1,2], n = 1
输出: [1]
解释: 链表为 1->2,删除倒数第1个节点(值为2),结果为 1
解题思路
初步思考
第一眼看到这个题目,可能会有同学想到先遍历一遍链表获取总长度,然后再遍历一次删除倒数第n个节点。这种方法虽然可行,但需要遍历两遍链表。有没有更优雅的解法呢?
答案是肯定的!我们可以使用双指针技巧,只需一次遍历就能完成任务。这种技巧在链表问题中非常常见,值得深入理解。
方法分析
方法一:双指针法(快慢指针)
思路 :
双指针法的核心思想是使用两个指针,让它们之间保持固定的距离。具体来说,我们让快指针先走n步,然后快慢指针一起移动,当快指针到达链表末尾时,慢指针就恰好指向待删除节点的前一个节点。
这种方法之所以高效,是因为:
- 只需遍历一次链表
- 不需要预先知道链表长度
- 代码简洁优雅
根据一个简单示例,通过图示展示思路:
以链表 [1,2,3,4,5], n=2 为例:
初始状态:快慢指针都指向头节点
head -> 1 -> 2 -> 3 -> 4 -> 5 -> null
↑
slow, fast
第一步:快指针先走n步(n=2)
head -> 1 -> 2 -> 3 -> 4 -> 5 -> null
↑ ↑
slow fast
第二步:快慢指针一起移动
head -> 1 -> 2 -> 3 -> 4 -> 5 -> null
↑ ↑
slow fast
第三步:继续移动,直到fast到达末尾
head -> 1 -> 2 -> 3 -> 4 -> 5 -> null
↑ ↑
slow fast
此时slow指向待删除节点(4)的前一个节点(3)
算法步骤:
- 创建哑节点(dummy node),使其指向链表头节点,用于处理边界情况
- 初始化快慢指针都指向哑节点
- 快指针先向前移动n步
- 快慢指针同时向前移动,直到快指针到达链表末尾
- 此时慢指针指向待删除节点的前一个节点,执行删除操作
- 返回哑节点的下一个节点
采用上面图示的示例,通过文字一步步的讲解该方法的实现过程:
以 [1,2,3,4,5], n=2 为例:
-
创建哑节点 dummy,指向头节点1
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> nullslow = fast = dummy
-
快指针先走2步
- fast 移动到节点3的位置
slow = dummy,fast = 3
-
快慢指针同时移动,fast 先到达节点5
- slow 移动到节点3的位置
- 此时 fast.next = null,循环结束
-
slow 指向节点3,执行删除
slow.next = slow.next.next即3.next = 4.next- 链表变为
dummy -> 1 -> 2 -> 3 -> 5 -> null
-
返回
dummy.next即 1
复杂度分析:
- 时间复杂度: O(L),其中L是链表长度,只需遍历一次
- 空间复杂度: O(1),只使用了常数个额外指针
代码实现
Python 实现:
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
"""
删除链表中倒数第n个节点
Args:
head: 链表头节点
n: 倒数第n个节点
Returns:
删除节点后的链表头节点
"""
# 创建哑节点,简化边界情况处理
dummy = ListNode(0, head)
slow = fast = dummy
# 快指针先走n步
for _ in range(n):
fast = fast.next
# 快慢指针一起移动,直到快指针到达末尾
while fast.next:
slow = slow.next
fast = fast.next
# 删除倒数第n个节点
slow.next = slow.next.next
return dummy.next
Java 实现:
java
/**
* Definition for singly-linked list.
* public 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 {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head);
ListNode slow = dummy;
ListNode fast = dummy;
for (int i = 0; i < n; i++) {
fast = fast.next;
}
while (fast.next != null) {
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;
return dummy.next;
}
}
总结与收获
知识点
- 哑节点(Dummy Node): 在链表头部添加一个哑节点,可以统一处理头节点被删除的边界情况,避免额外的条件判断
- 双指针技巧: 使用快慢两个指针,通过控制它们之间的距离差来定位目标位置,是链表问题中的常用技巧
- 一次遍历: 通过巧妙的指针移动,实现一次遍历完成所有操作
易错点
- 边界情况处理: 当要删除的是头节点时,如果没有哑节点,需要额外判断。本题使用哑节点统一处理
- 指针移动范围: 快指针只需要移动到倒数第n个节点,不需要移动到null,否则慢指针会少走一步
- 内存泄漏: 在Java中不需要手动释放,但在C++中需要注意删除的节点需要手动释放
优化思路
- 本方法已经是最优解,时间复杂度O(n),空间复杂度O(1)
- 可以考虑使用递归方式,但递归需要O(n)的栈空间,不如迭代方法高效