链表是一种常见的数据结构,由一系列节点组成,每个节点包含数据域和指针域。与数组不同,链表中的元素在内存中不是连续存储的,而是通过指针链接在一起。这种特性使得链表在插入和删除操作上具有优势,时间复杂度为O(1),但访问特定位置的元素需要O(n)的时间。
在解决"删除链表倒数第N个节点"问题时,我们面临几个关键挑战:
传统思路是两次遍历:第一次获取链表长度L,第二次定位到第L-N个节点。这种方法虽然直观,但需要两次遍历,时间复杂度为O(2n)。而双指针法则可以在一次遍历中解决问题,时间复杂度优化为O(n)。
双指针法的核心思想是维护两个指针,通过控制它们之间的相对距离来定位目标节点。具体到这个问题:
虚拟头节点(dummy node):这是一个技巧性设计,在原始链表前添加一个不存储实际数据的节点。它的主要作用是统一处理逻辑,特别是当需要删除的是实际头节点时,可以避免特殊处理。
快慢指针初始化:我们初始化两个指针fast和slow,都指向dummy节点。这样做的目的是让slow最终能停在待删除节点的前驱位置。
指针间距控制:先让fast指针向前移动n+1步。这个+1很关键,它确保当fast到达链表末尾时,slow正好停在待删除节点的前一个节点。例如要删除倒数第2个节点(n=2),fast需要先走3步。
同步移动阶段:然后同时移动fast和slow,直到fast为null。此时slow.next就是需要删除的节点。
提示:为什么fast要先走n+1步而不是n步?因为我们需要slow停在待删除节点的前驱位置,这样才能执行删除操作slow.next = slow.next.next。
让我们深入分析给出的Java实现代码:
java复制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);
dummy.next = head;
ListNode fast = dummy;
ListNode slow = dummy;
// fast先走n+1步
for(int i = 0; i <= n; i++) {
fast = fast.next;
}
// 同步移动fast和slow
while(fast != null) {
fast = fast.next;
slow = slow.next;
}
// 删除目标节点
slow.next = slow.next.next;
return dummy.next;
}
}
代码中的关键点:
new ListNode(0)创建虚拟头节点,其next指向真正的头节点for循环让fast先移动n+1步while循环中两个指针同步移动,直到fast为nullslow.next = slow.next.next跳过目标节点实现删除一个健壮的算法实现需要考虑各种边界情况。以下是需要特别关注的测试场景:
测试代码示例:
java复制public class Main {
public static void printList(ListNode head) {
ListNode current = head;
while(current != null) {
System.out.print(current.val + " ");
current = current.next;
}
System.out.println();
}
public static void main(String[] args) {
// 测试用例1:常规情况
ListNode head1 = new ListNode(1);
head1.next = new ListNode(2);
head1.next.next = new ListNode(3);
head1.next.next.next = new ListNode(4);
head1.next.next.next.next = new ListNode(5);
System.out.println("测试用例1 - 删除倒数第2个节点:");
System.out.print("原链表: ");
printList(head1);
Solution solution = new Solution();
ListNode result1 = solution.removeNthFromEnd(head1, 2);
System.out.print("结果链表: ");
printList(result1);
// 测试用例2:删除头节点
ListNode head2 = new ListNode(1);
head2.next = new ListNode(2);
head2.next.next = new ListNode(3);
System.out.println("\n测试用例2 - 删除倒数第3个节点(头节点):");
System.out.print("原链表: ");
printList(head2);
ListNode result2 = solution.removeNthFromEnd(head2, 3);
System.out.print("结果链表: ");
printList(result2);
// 测试用例3:单节点链表
ListNode head3 = new ListNode(1);
System.out.println("\n测试用例3 - 单节点链表删除:");
System.out.print("原链表: ");
printList(head3);
ListNode result3 = solution.removeNthFromEnd(head3, 1);
System.out.print("结果链表: ");
printList(result3);
}
}
时间复杂度分析:
空间复杂度分析:
虽然双指针法已经很高效,但在实际应用中还可以考虑以下优化点:
在实际编码中,容易遇到以下几个典型问题:
空指针异常:
删除错误节点:
内存泄漏(某些语言):
调试技巧:
删除链表倒数第N个节点的算法不仅在面试中常见,在实际开发中也有广泛应用:
算法扩展思考:
递归解法示例:
java复制class Solution {
private int count = 0;
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head == null) return null;
head.next = removeNthFromEnd(head.next, n);
count++;
return count == n ? head.next : head;
}
}
递归解法的特点:
虽然算法思想相同,但在不同语言中实现细节有所差异:
Python实现:
python复制class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
dummy = ListNode(0, head)
fast = slow = dummy
for _ in range(n + 1):
fast = fast.next
while fast:
fast = fast.next
slow = slow.next
slow.next = slow.next.next
return dummy.next
C++实现:
cpp复制struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* fast = dummy;
ListNode* slow = dummy;
for(int i = 0; i <= n; ++i) {
fast = fast->next;
}
while(fast) {
fast = fast->next;
slow = slow->next;
}
ListNode* toDelete = slow->next;
slow->next = slow->next->next;
delete toDelete; // 防止内存泄漏
return dummy->next;
}
};
各语言实现注意事项:
为了更直观地理解算法,我们以输入[1,2,3,4,5],n=2为例:
初始状态:
code复制dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
^fast
^slow
fast先移动3步(n+1=3):
code复制dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
^slow ^fast
同步移动直到fast为null:
code复制dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
^slow ^fast
此时slow指向节点3,slow.next是节点4:
code复制3 -> 4 -> 5
^slow
执行删除操作slow.next = slow.next.next:
code复制3 -> 5
最终链表为[1,2,3,5]
掌握这个算法后,可以解决一系列类似问题:
以"链表的中间节点"为例的Java实现:
java复制class Solution {
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}
这些题目都利用了指针的相对移动来高效解决问题,是链表类问题的常见技巧。