删除某个结点需要知道其前驱结点,单链表并不支持直接获取前驱结点,所以需要从头结点开始遍历链表,直到 p->next=q,说明 p 是 q 的前驱结点。
而删除倒数第N个结点,需要知道一共有多少个结点,才能定位到要删除结点的前驱,进行删除操作。
两次遍历
最直接的操作就是第一次遍历得到结点数量,第二次遍历进行删除:倒数第n个结点就是整数的L-n-1个结点,L为链表长度。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if(!head || !head->next)
return NULL;
auto lst = head;
int len = 1;
while (lst->next){
++len;
lst = lst->next;
}
lst = head;
for(int i = 1; i < len - n; i++)
lst = lst->next;
if(len == n)
return head->next;
if(n == 1)
lst->next = NULL;
else
lst->next = lst->next->next;
return head;
}
};
两次遍历思路简单,但是代码很容易出错,因为很容易漏掉极端情况,比如要删除链表头部或者链表只有一个结点,所以最好设置一个哑巴结点(带头链表),也就是“哨兵”。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
// 哨兵结点
auto dummy = new ListNode(0);
dummy->next = head;
auto lst = head;
int len = 0;
// 获取长度
while (lst != NULL){
++len;
lst = lst->next;
}
int target = len + 1 - n;
//利用哨兵结点遍历
lst = dummy;
while(target > 1){
lst = lst->next;
--target;
}
lst->next = lst->next->next;
// 如果return head,在仅有一个结点的极端情况会出问题。
return dummy->next;
}
};
一次遍历
双指针
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
// 哨兵结点
auto dummy = new ListNode(0);
dummy->next = head;
auto lnode = dummy;
auto rnode = dummy;
// 移动右指针使得左右指针相差n个位置
for(int i = 0; i < n; ++i){
rnode = rnode->next;
}
// 同时移动左右指针,使右指针指向链表最后一个元素,此时左指针指向需要删除元素的前一个位置
while(rnode->next != NULL){
rnode = rnode->next;
lnode = lnode->next;
}
lnode->next = lnode->next->next;
// 如果return head,在仅有一个结点的极端情况会出问题。
return dummy->next;
}
};