题目介绍
力扣19题:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
分析
链表中删除某个节点,其实就是将之前一个节点next,直接指向当前节点的后一个节点,相当于“跳过”了这个节点。
当然,真正意义上的删除,还应该回收节点本身占用的空间,进行内存管理。这一点在java中我们可以不考虑,直接由JVM的GC帮我们实现。
方法一:计算链表长度(二次遍历)
最简单的想法是,我们首先从头节点开始对链表进行一次遍历,得到链表的长度 L。
然后,我们再从头节点开始对链表进行一次遍历,当遍历到第 L-N+1 个节点时,它就是我们需要删除的倒数第N个节点。
这样,总共做两次遍历,我们就可以得到结果。
代码演示如下:
// 方法一:计算链表长度
public ListNode removeNthFromEnd1(ListNode head, int n){
// 1. 遍历链表,得到长度l
int l = getLength(head);
// 2. 从前到后继续遍历,找到正数第l-n+1个元素
// 定义一个哨兵节点,next指向头节点
ListNode sentinel = new ListNode(-1, head);
ListNode curr = sentinel;
for (int i = 0; i < l - n; i++){
curr = curr.next;
}
// 找到第l-n个节点
// 跳过第l-n+1个节点
curr.next = curr.next.next;
return sentinel.next;
}
// 定义一个获取链表长度的方法
public static int getLength(ListNode head){
int length = 0;
while ( head != null ){
length ++;
head = head.next;
}
return length;
}
复杂度分析
- 时间复杂度:O(L),其中 L 是链表的长度。只用了两次遍历,是线性时间复杂度。
- 空间复杂度:O(1)。
方法二:利用栈
另一个思路是利用栈数据结构。因为栈是“先进后出”的,所以我们可以在遍历链表的同时将所有节点依次入栈,然后再依次弹出。
这样,弹出栈的第 n 个节点就是需要删除的节点,并且目前栈顶的节点就是待删除节点的前驱节点。这样一来,删除操作就变得十分方便了。
代码演示如下:
// 方法二:使用栈
public ListNode removeNthFromEnd2(ListNode head, int n){
// 定义一个哨兵节点,next指向头节点
ListNode sentinel = new ListNode(-1, head);
ListNode curr = sentinel;
// 定义一个栈
Stack<ListNode> stack = new Stack<>();
// 1. 将所有节点入栈
while (curr != null){
stack.push(curr);
curr = curr.next;
}
// 2. 弹栈n次
for (int i = 0; i < n; i++)
stack.pop();
stack.peek().next = stack.peek().next.next;
return sentinel.next;
}
复杂度分析
- 时间复杂度:O(L),其中 L是链表的长度。我们压栈遍历了一次链表,弹栈遍历了N个节点,所以应该耗费O(L+N)时间。N <= L,所以时间复杂度依然是O(L),而且我们可以看出,遍历次数比两次要少,但依然没有达到“一次遍历”的要求。
- 空间复杂度:O(L),其中 L 是链表的长度。主要为栈的开销。
方法三:双指针(一次遍历)
我们可以使用两个指针 first 和 second 同时对链表进行遍历,要求 first 比 second 超前 N 个节点。
这样,它们总是保持着N的距离,当 first 遍历到链表的末尾(null)时,second 就恰好处于第L-N+1,也就是倒数第 N 个节点了。
代码演示如下:
// 方法三:双指针
public ListNode removeNthFromEnd(ListNode head, int n){
// 定义一个哨兵节点,next指向头节点
ListNode sentinel = new ListNode(-1, head);
// 定义前后双指针
ListNode first = sentinel, second = sentinel;
// 1. first先走n+1步
for (int i = 0; i < n + 1; i++)
first = first.next;
// 2. first、second同时前进,当first变为null,second就是倒数第n+1个节点
while (first != null){
first = first.next;
second = second.next;
}
// 3. 删除倒数第n个节点
second.next = second.next.next;
return sentinel.next;
}
复杂度分析
- 时间复杂度:O(L),其中 L是链表的长度。这次真正实现了一次遍历。
- 空间复杂度:O(1)。