题目描述
给定一个单向链表,要求删除从结尾数第n个结点,并返回修改后的表头。
链表结点的定义如下:
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
样例
给定数组单向链表 1->2->3->4->5 ,以及 n = 2 ,修改后的链表为 1->2->3->5。
Note
- n总是合法的。
- 尝试使用一次遍历完成本题。
思路及算法
算法1 保护结点法
- 在头结点之前添加保护结点。
- 设置两个指针first和second,均指向保护结点。
- first指针先向后移动n+1个结点。
- 然后first和second指针同时向后移动,直到first指针指向空,此时second结点指向的下一个结点需要删除。
解释:
始终保持两个指针之间间隔n个结点,在first到达终点时,second的下一个结点就是从结尾数第n个结点。
这样做可以回避掉如果链表只有一个结点,需要单独处理的问题。在处理有对头结点进行操作的问题时,可以创建虚拟结点来刚方便地处理。
还要注意的是,删除一个结点时,我们需要这个待删除结点的前驱结点,所以要使second指向待删除结点的前驱,而不是指向待删除结点本身。
时间复杂度
O(L)
空间复杂度
O(1)
C++ 代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* ext_head = new ListNode(0);
ext_head -> next = head;
ListNode* first = ext_head;
ListNode* second = ext_head;
for (int i = 0; i <= n; i++)
first = first -> next;
while (first != NULL) {
first = first -> next;
second = second -> next;
}
second -> next = second -> next -> next;
return ext_head -> next;
}
};
作者:wzc1995
链接:https://www.acwing.com/solution/LeetCode/content/67/
来源:AcWing
算法2 无保护指针的双指针法
- 设置first和second两个工作指针,均指向头结点。
- first指针向后移动n个结点。
- 若second没有指向空,则first和second均向后移动一个结点,并设置pre指针,指向头结点。
- pre、first和second指针均向后移动,直到first指向空,此时second即为被删除的结点,通过pre指针将其删除。
解释:
两个工作指针中间间隔n-1个结点,当second指向空时,first指向的就是待删除结点。为了删除first所指向的结点,需要再额外建立一个pre指针指向first的前驱。
需要注意的是,在first、second同时向后移动前,要设置出pre指针,但是此时first就是头指针,是没有前驱的。所以需要在工作指针同时向后移动时,先移动一步,使得first所指结点有前驱。还要注意如果second向后移动n个结点后,已经指向空,则要删除的是头结点,此时first不存在前驱。
时间复杂度
O(L)
空间复杂度
O(1)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* first = head;//前进n次的工作指针
ListNode* second = head;//当前指针
if(!head) return NULL; //长度为0
else{
int num = 0;
for(int i = 0;i<n;i++){
first = first->next;
}
//一共有三个指针,但对前驱指针的第一次操作和之后的操作不一样
if(!p){
//如果n刚好是链表的长度
return head->next;
}
else{
ListNode* pre = head;
second = second->next;
first = first->next;
while(first){
pre = pre->next;
second = second->next;
first = first->next;
}
pre->next = second->next;
return head;
}
}
}
};
算法三 对算法二的改进
解释:
算法二的弊端在于first、second要在循环外做一次移动。如果把循环的条件改成first->next不为空,则可以省去pre指针的构建。
值得注意的是,链表长度为1的情况是链表长度等于n的子集,进而保证了first不为null。
时间复杂度
O(L)
空间复杂度
O(1)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if (!head) {
return nullptr;
}
ListNode* fast = head;
ListNode* slow = head;
for (int i = 0; i < n; ++i) {
fast = fast->next;
}
if (!fast) {
auto temp = head->next;
delete head;
return temp;
}
while (fast->next) {
fast = fast->next;
slow = slow->next;
}
auto temp = slow->next;
slow->next = slow->next->next;
delete temp;
return head;
}
};
总结
保护指针法应该说是比较套路的方法,比之后两种方法,能省去一些边界条件的判定。