19. 删除链表的倒数第N个节点
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:
给定的 n 保证是有效的。
进阶:
你能尝试使用一趟扫描实现吗?
解法1:两次遍历,单指针
第一遍遍历计算长度,第二遍删除节点,倒数第N个,就是正数第len-N+1个,即找第len-N个节点完成删除操作,站在第一个节点上,只需走len-N-1步。
class Solution {
int listLen(ListNode* head) {
if (NULL == head)
return 0;
int len = 0;
while (head) {
++len;
head = head->next;
}
return len;
}
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if (NULL == head || n < 0)
return head;
int len = listLen(head);
if (len < n)
return head;
else if (len == n)
return head->next;
n = len - n - 1;
ListNode *first = head, *tmp;
while (n--)
first = first->next;
tmp = first->next->next;
delete(first->next);
first->next = tmp;
return head;
}
};
解法2:一次遍历,双指针,无哨兵
无法计算长度,用双指针一个先走,一个后走,重点在于先走的指针走几步,后面的指针再走。后面的指针需要停在目标删除节点的前一格节点上。考虑到有可能需删首节点,所以需要特殊处理这种情况。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if (head == NULL || n <= 0)
return head;
ListNode* first = head, *second = head; //两指针都在head上
while (n-- && first) //以first要停的位置推需要在首节点上先走n步
first = first->next;
while (first && first->next) { //first要停在最后一个非空节点上
first = first->next;
second = second->next;
}
if (n > 0 && NULL == first) //n大于链表长度
return head;
ListNode *tmp;
if(first) { //first如愿,得到目标删除节点的前一节点second
tmp = second->next->next;
delete second->next;
second->next = tmp;
return head;
} else { //否则,需要删除头结点
tmp = head->next;
delete head;
return tmp;
}
}
};
解法3:一次遍历,双指针,有哨兵
考虑到有可能需删首节点,用哨兵处理这种情况。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if (NULL == head || n <= 0)
return head;
ListNode dummy(0);
dummy.next = head;
ListNode *first = &dummy, *second = &dummy; //起始点是哨兵节点
while (n-- && first)
first = first->next;
if (n > 0 && NULL == first)
return head;
while (first && first->next) { //first要停在最后一个非空节点上
first = first->next;
second = second->next;
}
first = second->next->next;
delete second->next;
second->next = first;
return dummy.next;
}
};
可能有细心的同学会发现,该实现跟解法2没有哨兵实现相比,两指针的起点不一样,实际上将上续实现中指针初始化改为ListNode *first = head, *second = head;,无需修改其他依旧是正确的。因为second和first起点一样,first少走一步,second也少走一步,所以second之后同步走的时候,这一步的差就一起携手走完了,重点是两个指针的相对步数差,而不是两个指针的起点。
一趟扫描的重点就是双指针的第一个指针p先走几步,跟p要停在哪有关,解法2和解法3是p要停在链表最后一个非空节点上,如果p要停在空节点上,这个步数就需调整,后续一系列判断也可能有改变。这个决定跟个人习惯有关,读者可自行探索实现。推导过程就是:先走的指针要停哪,后走的指针要停哪,计算期间的步数差。
用哨兵实现会相对简单易懂,建议使用哨兵,少出错,程序可读性高。注意哨兵的细节处理和垃圾回收。