给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
C++代码1:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if(head == NULL) return head;
ListNode* s=head, *f=head; // 快慢指针
/*************确定链表的长度******************/
int cnt = 0;
for(;f->next!=NULL && f->next->next != NULL;s = s->next,f=f->next->next){
cnt ++;
}
cnt ++;
if(f->next==NULL){
cnt *= 2;
cnt -= 1;
}
else{
cnt *= 2;
}
/*****************************************************/
if(cnt == 1){
return NULL;
}
ListNode* p;
if(n <= (int)(cnt / 2)){
int d = cnt/ 2 - n;
p = s;
for(int i = 0;i<d;i++){
p=p->next;
}
}
else{
int d = cnt - n;
p = head;
if(d == 0) return head->next; // 特殊情况特殊对待
for(int i = 0;i<d-1;i++){
p=p->next;
}
}
p->next = p->next->next;
return head;
}
};
这段代码思路是这样的:
- 首先得到链表的长度
通过遍历链表,求得链表的长度cnt,但是这里用到了快慢指针的方法来求长度,将遍历所用时间缩短了50%。 - 判断n的大小
如果n大于链表长度的一半,那么需要删除的结点一定位于链表中点的左侧,这时从head结点开始遍历,找到需要删除的结点;如果n小于链表长度的一半,那么需要删除的结点一定位于链表中点的右侧,这时,可以从慢指针所指向的结点开始访问。遍历访问最坏的情况是访问了链表长度的一半。 - 删除结点
整个过程最坏的情况是遍历的结点个数为链表的长度。上面分析的是宏观的角度,其实在自己编写代码的时候有很多细节需要考虑: - 快慢指针法
如果之前没有用过快慢指针,可能快慢指针的思路很容易懂,但是具体实现的时候会遇到一些问题,比如:快慢指针的初始值是全为head好还是慢指针为head,快指针为head->next好?(我这段代码全为head);既然使用快慢指针法,在慢指针到达中点的时候,快指针到达最后一个结点还是倒数第二个结点取决于指针长度为奇数还是偶数,如果为奇数,当快指针到达最后一个结点的时候就应该让循环终止;如果为偶数,当快指针到达倒数第二个结点的时候就应该让循环终止。当循环终止的时候,慢指针的指向也取决于链表长度的数值,如果为奇数,那么慢指针最终指向中间结点,如果为偶数,慢指针最终指向为link[n/2]。 - 确定链表的最终长度
快慢指针循环结束之后,此时计算的链表长度只是一半的长度,真实的长度也需要根据链表长度值为奇数还是偶数来确定。如果是奇数,链表长度为2n-1;如果是偶数,链表长度为2n。 - 确定得到待删除结点的初始位置
确定要从链表头开始还是中间点开始,就要判断n的大小,当n小于cnt/2的时候,从中点开始;当n>cnt/2的时候从head开始。这样说可能觉得很容易,其实一开始我挺纠结到底是和(cnt+1)/2比较还是和(cnt) / 2比较为好。 - 从head/中点开始需要走多少步?
代码里我用的是s表示慢指针,能从s最后的位置开始搜索必须要满足待删除结点位于s之后,在s的前面或者s本身都是无法从s开始实现删除的。所以需要自己模拟几步,确定计算要走多少步的数学公式。在两种情况(head和中点)中,代码里都有一个循环,这个循环中我将p=p->next;放在了for的内部,因为如果放在i++后面,即便某一次循环条件不满足了,p还是会移动一步,就会导致跳出循环之后p指向的结点就是需要删除的结点,但是我们对于p结点的前驱结点无法确定,除非重新遍历一遍。 - 特殊情况特殊对待
输入的链表为[]、[a]的情况比较特殊,需要特殊对待。
C++代码2
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if(!head || !head->next)
return nullptr;
ListNode *newHead = new ListNode(0, head);
ListNode *first = newHead;
ListNode *second = newHead;
while(n--) {
first = first->next;
if(!first) break;
}
if(!first) {
delete newHead;
return nullptr;
}
while(first->next) {
first = first->next;
second = second->next;
}
second->next = second->next->next;
first = newHead->next;
delete newHead;
return first;
}
};
这段代码不是我写的,感觉代码的结构比我的规范多了,有很多地方可以学习的,所以放在这里。