记录新手小白的做题过程。
题目:给你一个链表,删除链表的倒数第
n
个结点,并且返回链表的头结点。
感觉这道题的思路很简单啊,先遍历到尾结点,然后从尾结点遍历回来。。。
咦,不可能从尾结点遍历回来啊。
那就先将链表反转,然后再从新的头节点开始遍历,删除第n个结点,然后一直遍历到尾,即原来的头节点,不知道题目有没有要求结点顺序不能变,先试试看吧。
第一次尝试:
研究了一下给出的用例,应该事结点顺序不能变,那就记住头节点,再将结点反转回来就好了。
学到了力扣上缩进的快捷键,CTRL+[。
/**
* 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) {
ListNode *p=nullptr;
ListNode *curr=head;
while(curr){
ListNode *next=curr->next;
curr->next=p;
p=curr;
curr=next;//第一次反转
}
ListNode *dummyhead=new ListNode;//创建虚拟头节点,记得最后需要删掉
dummyhead->next=p;
p=dummyhead;
while(p!=nullptr){//开始遍历
n--;
if(n==0){//不需要额外考虑最后一个结点,prev的next可以指向空
ListNode *prev=p->next;
p->next=prev->next;
delete prev;
}
}//删除结束,//这里变成了死循环
ListNode *head2=p;//记录头节点
curr=p;//再次赋值结点用于反转
p=nullptr;
while(curr->next!=nullptr){//选择curr->next不为空,而不是curr,是因为最后一个curr的next是虚拟头节点
ListNode *next=curr->next;
curr->next=p;
p=curr;
curr=next;//第一次反转
}
delete dummyhead;//删除虚拟头节点
curr->next=nullptr;//指向空
return head2;
}
};
写出的代码如上,但有死循环,在第二个while里,我没有改动p
研究了一下啊,我发现第二个循环过后,我就已经弄丢P了,p是在为0才退出循环的,而我又把他设为头节点
/**
* 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) {
ListNode *p=nullptr;
ListNode *curr=head;
while(curr){
ListNode *next=curr->next;
curr->next=p;
p=curr;
curr=next;//第一次反转
}
ListNode *dummyhead=new ListNode(0,p);//创建虚拟头节点,记得最后需要删掉
p=dummyhead;
while(p->next!=nullptr){//开始遍历
n--;
if(n==0){//不需要额外考虑最后一个结点,prev的next可以指向空
ListNode *prev=p->next;
p->next=prev->next;
delete prev;
}
p=p->next;
}//删除结束
ListNode *head2=p;//记录头节点
curr=p;//再次赋值结点用于反转
p=nullptr;
while(curr->next!=nullptr){//选择curr->next不为空,而不是curr,是因为最后一个curr的next是虚拟头节点
ListNode *next=curr->next;
curr->next=p;
p=curr;
curr=next;//第一次反转
}
delete dummyhead;//删除虚拟头节点
curr->next=nullptr;//指向空
return head2;
}
};
改了一下没有死循环了,但是还说解答错误。
官方代码:
看了一下官方的,发现我的思路太复杂了2333.
方法一:计算链表长度
class Solution {
public:
int getLength(ListNode* head) {//写了个函数计算长度
int length = 0;
while (head) {
++length;//没有区别
head = head->next;
}
return length;
}
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);//虚拟头节点
int length = getLength(head);
ListNode* cur = dummy;
for (int i = 1; i < length - n + 1; ++i) {
cur = cur->next;
}
cur->next = cur->next->next;
ListNode* ans = dummy->next;
delete dummy;//只删除了头节点,没有删掉要删除的结点
return ans;
}
};
在这里长了个记性,只有new出来的才需要用delete删除,直接创建的ListNode *p=dummyhead不需要删除。
class Solution {
public:
int length(ListNode *head){
int length=0;
while(head){
++length;
head=head->next;
}
return length;
}
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *dummyhead=new ListNode(0,head);
int len=length(head);
ListNode *curr=dummyhead;
ListNode *head1=dummyhead->next;//新头节点赋值的操作 1
for(int i=1;i<len-n+1;i++){
curr=curr->next;
}
ListNode *p=curr->next;
curr->next=p->next;// 2
delete dummyhead;
return head1;
}
};
看到官方的代码后我自己打了一个不完全相似的,但是只对了一部分。
我研究了一下,为什么我的不能完全正确。
最重要的一个区别是,我将head1(新的头节点)在最先开始就赋值了虚拟头节点的next。而官方是在删除结点之后才赋值新的头节点。
我忽略了头节点有可能也会被删除,直接默认为新的头节点。放到位置2才是正确的,不能放到位置1。
从这里我又学到了,代码并不如所想的那样,要多打才能有深刻体会。、
方法二:栈
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);//虚拟头节点
stack<ListNode*> stk;//创建栈
ListNode* cur = dummy;
while (cur) {
stk.push(cur);//放入栈中
cur = cur->next;
}
for (int i = 0; i < n; ++i) {//i++与++i没有区别
stk.pop();//取出,取出n个结点,这样最顶上的结点就是前置结点
}
ListNode* prev = stk.top();//取要删除结点的前置结点
prev->next = prev->next->next;//删除
ListNode* ans = dummy->next;//头节点
delete dummy;
return ans;
}
};
因为现在还没有学过栈,所以花点时间研究了一下。
栈在这里真的很好用,而且简单易懂。由于先进后出的特性,将整个链表放进去后,再取出第k个就是我们需要的。由于要删除第N个,还要取栈顶(前驱结点)
方法三:双指针法
我发现双指针法在很多地方都有运用,看来要好好掌握。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);//虚拟头节点
ListNode* first = head;//前面的指针
ListNode* second = dummy;//后面的指针,从虚拟头节点,找到待删除结点的前置结点
for (int i = 0; i < n; ++i) {//其实两者的距离不止是n了
first = first->next;
}
while (first) {
first = first->next;
second = second->next;
}
second->next = second->next->next;
ListNode* ans = dummy->next;
delete dummy;
return ans;
}
};
双指针的代码很好理解,就是先将两个具有相同距离的指针同步向前移动,当前面的指针遍历到链表尾部时,后面的指针指向的就是要删除的元素。
哇,双指针的做法也很精妙。
学到了。
这道题收获了很多好方法啊。
果然要多刷题,我的思路实在是不够简单。加油!