LeetCode初级算法之链表:删除链表中的节点和删除链表中的倒数第N个节点

这是LeetCode初级算法链表系列的前两个题,由于都是删除链表中的节点,所以顺便整理到一块了。

删除链表中的节点

请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。

现有一个链表 – head = [4,5,1,9],它可以表示为:

示例 1:
输入: head = [4,5,1,9], node = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
示例 2:
输入: head = [4,5,1,9], node = 1
输出: [4,5,9]
解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.
说明:
链表至少包含两个节点。
链表中所有节点的值都是唯一的。
给定的节点为非末尾节点并且一定是链表中的一个有效节点。
不要从你的函数中返回任何结果。

由于是第一次在LeetCode上刷这个题,拿到之后有点懵,因为自己曾经做这种题目的时候,都是一个写好的链表,然后会有一个头结点,然后删除的时候会给出头结点和第几个节点,然后删除,所以对于这种问题有种思维定式了。 还以为这个题少参数。
其实这个题提供了另外一种删除节点的思路:交换删除
其实,给出的就是删除的节点号,我们做的就是在这个基础上进行删除,所谓的交换删除就是把它后面的值给它,等价到删除它后面的节点,而不用和之前那样,先遍历找到该节点的前面那个节点,再交换指针。
所以,下面的两行代码即可搞定。

class Solution {
public:
    void deleteNode(ListNode* node) {
        node->val = node->next->val;
        node->next = node->next->next;
    }
};

所以有时候做题的时候,见多识广很重要。不要限制住思维。

删除链表中的倒数第N个节点

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:
给定的 n 保证是有效的。
进阶:
你能尝试使用一趟扫描实现吗?

思路一:虚拟结点 + 暴力破解

这个题第一次刷的时候也是定式了一下,以为head是一个附加的那种头结点,其实不是,head就是第一个节点了,默认是没有数据结构上学的那种头结点的,所以如果想把一个节点的操作和很多个节点的操作统一起来,最好的方式就是加一个虚拟节点在前面
加上之后,这个题可以用暴力进行破解,但是不太提倡。
思路: 暴力破解的思路就把倒数的,转换成正数多少个。 所以先遍历第一遍,数一共多少个节点,然后用这个数减去输入的n,就是要删除的前面那个节点的序号,再遍历第二遍,找到那个序号,改变指针指向后面的后面即可。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        
        ListNode *dummyHead = new ListNode(0);
        dummyHead->next = head;
        
        int cou = 0;
        ListNode *p = dummyHead;
        ListNode *q = dummyHead;
        while (p->next)
        {
            p = p->next;
            cou++;
        }
        
        int delnodeloc = cou-n;
        
        for (int i=0; i<delnodeloc;i++)
            q = q->next;
        
        q->next = q->next->next;
        return dummyHead->next;
    }
};

这个方法会遍历两遍。上面添加头结点的方式很重要。

思路二: 快慢指针法

这个题第一次拿到想到的就是这个方式,但是忽略的问题就是head直接是第一个节点,不是虚拟的头结点,这样带来的问题就是如果只有1个的话,删除这一个,和如果多个的话,删除会不一样,因为这个head有可能会被删除,并且删除的时候,如果是只有这一个节点,会返回空,但是如果不是这一个节点,需要把head指向后面的。讨论起来也挺麻烦的,不过费劲一波心思,用这个方法也是a了。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode *p, *q;
        p = q = head;

        int cou=1;
        while(q->next && cou<n)
        {
            q = q->next;
            cou++;
        }

        ListNode *p_prev = NULL;
        while (q->next)
        {
            q = q->next;
            p_prev = p;
            p = p->next;
        }

        if (p_prev)
            p_prev->next = p->next;
        else
        {
            if(n == 1)
                head = NULL;
            else
                head = head->next;

        }


        return head;
    }
};

这个双指针的思路很好,但是用在这种没有虚拟的节点的链表上,潜在的威胁会多出很多,因为稍微不注意,控制不好,指针就访问非法内存了,这是很头疼的一件事,在这只是记录,但这种方式也不提倡。

思路三: 虚拟节点+快慢指针法

这个版本的思路比较好,也是解决这个问题的常规方法,还仅仅遍历一遍,所谓的快慢指针,就是先让一个快的指针遍历到第n个的位置,慢指针指着开头的问题,然后一块向后移动,但快指针指向到了最后一个位置的时候,慢指针这时候正好到达倒数第n的位置,删除即可。
看下面的一个动画演示:(在题解上找到的)

代码如下:

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode *dummyHead = new ListNode(0);
        dummyHead->next = head;
        
        ListNode *p = dummyHead;
        ListNode *q = dummyHead;
        
        for (int i=0; i<n+1; i++)
            q = q -> next;
        
        while (q)
        {
            q = q->next;
            p = p->next;
        }
        
        ListNode *delNode = p->next;
        p->next = delNode->next;
        delete delNode;
        
        ListNode *retNode = dummyHead->next;    // 因为可能head已经被删除了
        delete dummyHead;
        
        return retNode;
    }
};

这个思路是很好的, 一次遍历即可搞定。

Python代码:

class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        
        if not head or not head.next:
            return 
        
        # 哑结点
        dummy = ListNode(-1)
        dummy.next = head
        
        p = head
        cou = 0
        while cou < n:
            p = p.next
            cou += 1
        
        
        pre = dummy
        q = pre.next
        while p:
            p = p.next
            pre = q
            q = pre.next
            
        pre.next = q.next
        
        return dummy.next

总结

通过这两个题目,基本上可以回忆起链表的特点和结构,只不过LeetCode上的没有所谓的虚拟头结点,需要自己创建一个,这样才能把只有一个节点的情况和多个节点的情况的操作统一起来。
其次,对于一个链表来说,比较好的习惯是声明指针的时候,如果没有具体的指向,先指向空,如果多余的空间没有用的话,要记得释放掉。
最后,链表这个地方,尤其是在循环里面,如果控制不好终止的条件,很容易出现非法访问内存的情况,所以这个情况一定要注意。

发布了66 篇原创文章 · 获赞 67 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/wuzhongqiang/article/details/103333114
今日推荐