19_删除链表的倒数第N个结点

这篇博客记录了一位新手在解决LeetCode链表问题时的心路历程,探讨了多种方法,包括反转链表、使用栈、双指针法来删除链表倒数第n个节点。博主分享了自己在编程过程中遇到的错误和学习到的新知识,强调了简洁代码的重要性,并对比了官方解决方案。
摘要由CSDN通过智能技术生成

记录新手小白的做题过程。

题目:给你一个链表,删除链表的倒数第 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;
    }
};

双指针的代码很好理解,就是先将两个具有相同距离的指针同步向前移动,当前面的指针遍历到链表尾部时,后面的指针指向的就是要删除的元素。

哇,双指针的做法也很精妙。

学到了。


这道题收获了很多好方法啊。

果然要多刷题,我的思路实在是不够简单。加油!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值