问题描述
给定一个链表,删除链表的倒数第n个节点,并返回链表的头节点。
示例:
输入: head = [1, 2, 3, 4, 5], n = 2
输出: head = [1, 2, 3, 5]
说明:
给定的n
保证是有效的
问题分析
这道题目的难度,力扣官网给出的难度为“中等”。通常中等难度的问题需要将问题拆解为两部分,或者通常为简单程度题目的进一步综合。
我们通过题干可以了解到,这一题是删除倒数第n
个节点,这里的重点为删除,那么我们需要确定删除的对象或具体删除的位置,这样可以将题干具体为以下问题:
- 如何定位删除的节点?(这里是倒数第
n
个节点) - 如何删除此节点?
关于问题1,我们知道,对于一个链表,我们只知道其首节点,其他任何信息如长度均不可知,此时我们需要有单独的实现定位节点功能的代码。当我们不知道长度等具体情况的时候,我们需要有一个“探路兵”,告诉我们这条路具体多长l
,然后我们需要往前走l-n
的距离,此时停止,位置恰好就为倒数的n
处。
边界条件考虑
题目中并没有完全说明n
的取值范围,只指明了有效,则我们必须把边界条件考虑在内:
- 当需要删除的节点为首节点时
我们易知此时采用哨兵机制(具体可参照此篇) - 当需要删除的节点为尾节点时
当需要删除的节点为尾节点时,我们的规则保持统一,故不需要进一步的调整。
步骤罗列
- 初始化“探路兵”,即一个指针
scoutP
和哨兵节点dummyHead
; - 令
scoutP
遍历一遍链表,给出确定的链表长度l
; - 再对链表进行遍历,此时给定遍历的长度为
l-n
; - 给定的长度用完,则为倒数第
n
个节点,删除此节点(删除操作可参照文章1,2)。
解题代码1
public static ListNode solutionWithP(ListNode head, int n) {
if (head == null) {
return head;
}
//1. init pointers and dummyHead
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode scoutP = dummyHead;
int len = 0;
// 2. go through linkedlist to ensure it's length
while (scoutP.next != null) {
len++;
scoutP = scoutP.next;
}
int step = len - n;
scoutP = dummyHead; //re-use of scoutP
// 3. go through linkedlist for len-n step
while(step-- > 0)
scoutP = scoutP.next;
// 4. deleted specified node
scoutP.next = scoutP.next.next;
return dummyHead.next;
}
复杂度分析
-
时间复杂度:
这里我们对链表进行了不止一次的遍历,第一次为确定链表长度O(N);
第二次为定位需要删除的节点位置O(N - n));最坏情况下为删除链表最后一个节点,此时n
为1,遍历了2N-n个节点;最好情况下为删除链表第一个节点,此时n
为N,遍历了N+1个节点。
时间复杂度均为O(N)。 -
空间复杂度:
我们没有初始化或设置额外的辅助容器,所以这里空间复杂度为O(1);
进阶
这里可不可以对数据遍历的次数减少??
在解题代码1中我们分析,其遍历次数不止一次,且最坏情况下需要遍历两次,当我们一个指针需要遍历两次的时候,这时候可以思考,能不能增加指针数量来使得遍历次数减少呢?
进阶解题思路
我们需要找出倒数第n
节点的位置,这里可以通过控制两个指针间距来定义这个n
长度,这里我们设fastP
与slowP
, 当快指针到达链表尾部时,此时slowP
所在的位置就是应该删去节点的前一个节点。
这时候,我们再进一步梳理思路则有:
步骤罗列
- 定义两个指针和哨兵节点;
- 使得快节点与慢节点间隔为
n
; - 两个指针同时开始遍历,快指针到达链表尾部时,停止;
- 慢指针更新其后续节点,返回哨兵节点的下一个节点。
解题代码2
public static ListNode solutionWithTwoP(ListNode head, int n) {
if (head == null) {
return head;
}
//1. init pointers and dummyHead
ListNode dummyHead = new ListNode(-1);
dummyHead.next = head;
ListNode slowP = dummyHead;
ListNode fastP = dummyHead;
//2. init the fixed gap of n
for(int i = 1; i < n+1; i++)
fastP = fastP.next;
//3. slowP start to move
while (fastP.next != null) {
slowP = slowP.next;
fastP = fastP.next;
}
//3. delete specified node
slowP.next = slowP.next.next;
return dummyHead.next;
}
复杂度分析
-
时间复杂度:
这里只对倒数第n
个节点之前的数据进行了遍历,故其时间复杂度为O(L),这里L=N-n
-
空间复杂度
没有额外的容器要求,即只要求常量级的额外空间,故空间复杂度为O(1)。
GitHub代码
具体完整可运行文件参见GitHub。