关于有关找寻链表结点位置问题的分析

1.删除链表的倒数第n个节点

leetcode题目链接

关于删除链表中倒数第n个节点的问题,一种有效的方法是使用双指针技术,具体地说,是使用两个指针:左指针(L)和右指针(R)。通过这种方法,我们可以充分利用给定的位置信息(即n),以高效地找到并删除目标节点。以下是该方法的详细步骤:

  1. 初始化指针:首先,我们将左指针L和右指针R都初始化为指向链表的头节点。这是因为我们需要从链表的开始处出发来查找目标节点。

  2. 移动右指针:由于我们的目标是删除倒数第n个节点,我们需要找到这个节点的前驱节点。为了达到这个目的,我们让右指针R先向前移动n+1步。这样做的原因是,当右指针R最终指向链表的末尾(NULL)时,左指针L将会指向倒数第n+1个节点,即目标节点的前驱节点。如果链表的长度正好是n,这意味着要删除的是头节点,此时右指针R将会直接指向链表的末尾(NULL),而左指针L仍指向头节点。

  3. 同时移动两个指针:在右指针R已经提前移动了n+1步之后,我们开始同时移动左指针L和右指针R,两者都每次向前移动一步。这一过程持续进行,直到右指针R指向链表的末尾(NULL)。此时,左指针L将正好位于要删除节点的前驱节点。

  4. 删除节点:找到目标节点的前驱节点后,我们接下来需要修改这个前驱节点的next指针,使其指向要删除节点的下一个节点。具体来说,如果左指针L指向的节点是要删除节点的前驱节点,我们就将L->next指向L->next->next,从而实现删除操作。

  5. 特殊情况处理:如果要删除的节点是链表的头节点(即n等于链表的总长度),那么在上述步骤中,我们需要额外处理这种情况。在这种情况下,我们直接将链表的头节点更新为头节点的下一个节点即可。

通过以上步骤,我们可以有效地在单次遍历中删除链表中的倒数第n个节点,而无需首先计算链表的总长度。这种双指针方法不仅提高了效率,而且还简化了操作过程。

简单图解如下所示:

假设有一个具有五个结点的链表,我们要找到并删除倒数第二个结点。为了实现这一目标,我们可以采用双指针方法,具体步骤如下:

  1. 初始化双指针:首先,我们定义两个指针,左指针(L)和右指针(R),并将它们都初始化为指向链表的头节点。这是因为我们需要从链表的起始位置开始寻找目标节点。

  2. 移动右指针:由于目标是删除倒数第二个结点,根据双指针方法,我们需要让右指针R先行移动n+1步,其中n为2(因为是倒数第二个)。因此,我们让右指针R向前移动3步。这样做的目的是要使得当右指针R到达链表末尾(NULL)时,左指针L能够正好指向目标节点的前驱节点。

  3. 同时移动两个指针:在右指针R已经提前移动了3步之后,我们开始同时移动左指针L和右指针R,每次都向前移动一步。这一过程持续进行,直到右指针R指向链表的末尾(NULL)。此时左指针L将会指向倒数第三个节点,即倒数第二个节点的前驱节点。

  4. 删除目标节点:找到倒数第二个节点的前驱节点后,我们需要进行删除操作。具体来说,我们将左指针L所指向的节点的next指针指向其next节点的next节点。即如果左指针L指向的节点是倒数第三个节点,我们就将L->next指向L->next->next,从而跳过了倒数第二个节点,实现了其删除。

 按照以上思路建立起来的模型, C++实现代码如下:

class Solution {
public:
     ListNode* removeNthFromEnd(ListNode* head, int n) {
    struct ListNode* L = head;
    struct ListNode* R = head;
    
    for (int i = 0; i < n+1; ++i) 
    {
        R = R->next;
    }
    
    while (R) 
    {
        R = R->next;
        L = L->next;
    }
    struct ListNode* dummy=L->next;
    L->next = L->next->next;
    delete(dummy);
    return head;
    }
};

提交上述代码到LeetCode进行分析后遇到了内存报错,这通常意味着代码中存在内存管理相关的问题。 

经通过报错的数据进行排查,遇到的错误是因为在尝试删除从链表末尾数第n个节点时,代码没有正确处理一些特殊情况。具体地说,当R指针在循环中移动n+1次后,如果输入的n等于链表的长度,R将会是nullptr。这种情况下,代码将无法进入while循环,因为R已经是nullptr了。随后,当尝试通过L->next = L->next->next;删除节点时,由于L仍然指向头节点(因为while循环未执行),这将导致错误地尝试删除头节点的下一个节点,而当链表只有一个结点时,头结点的下一个结点是不存在的,因此会导致内存泄漏等问题。

此外,代码中还缺少处理当要删除的节点恰好是头节点时的逻辑。为了修复这个问题并处理所有特殊情况,我们可以引入一个哑节点(dummy node)作为新的头节点,这样即使需要删除原始链表的头节点,我们也有一个稳定的节点指针引用来操作链表。

对LeetCode错误案例进行分析,引入哑节点(dummy node)图解如下:

当n与链表长度相同时,R指针移动n+1个结点长度

由于新头结点(哑节点)的引入,原来的链表从长度(n)变为了(n+1),此时如果输入的(n)等于原链表长度,首先移动的指针(我们称之为R)将会指向最后一个节点的下一个位置,即nullptr,而不是最后一个节点。这种设计可以有效地避免输入的(n)等于链表长度时导致的问题,因为哑节点确保了即使需要删除的是头节点,也能够通过L指针(它最终会指向哑节点)来正确地执行删除操作。在这种情况下,L指针最终将指向哑节点,而R指针在经过(n+1)次移动(包括从哑节点到原始头节点的一次移动和随后沿链表向前移动的(n)次移动)后会指向nullptr。这时,L->next实际上指向原链表的头节点,即我们需要删除的节点。通过更新L->next来跳过这个节点,我们就能够实现从链表末尾数第(n)个节点的删除。这样,无论是需要删除头节点还是链表中的其他节点,我们都有一个统一且稳定的处理流程。

简而言之,引入哑节点并使得链表长度视觉上增加1,使得算法能够统一处理包括删除头节点在内的所有情况,从而避免了当删除节点正好是头节点时可能出现的边界问题。

修正后的代码如下: 

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        ListNode* L = dummy;
        ListNode* R = dummy;
        for (int i = 0; i < n + 1; ++i) {
            R = R->next;
        }

        while (R != nullptr) {
            R = R->next;
            L = L->next;
        }

        ListNode* toDelete = L->next;
        L->next = L->next->next;
        delete toDelete;

        ListNode* newHead = dummy->next;
        delete dummy;
        return newHead;
    }
};

提交到 LeetCode 结果如下:

2.链表的中间结点

leetcode题目链接

对于寻找链表中间节点这类问题,由于没有一个明确且固定的节点位置标识中间,且该位置随着链表长度的变化而变化,我们通常采用双指针技术,其中一个指针称为快指针(记为R),另一个称为慢指针(记为L)。为了保证两者的相对位置,我们让快指针R每次移动两步,而慢指针L每次移动一步。这样,当链表长度为奇数时,快指针R走到链表尾部(即R->next == nullptr)时,慢指针L就恰好指向链表的中间节点。

在实际应用中,我们还需要考虑链表长度为偶数的情况。对于长度为偶数的链表,当快指针R的下一个节点是nullptr(即R->next == nullptr)时,慢指针L将位于中间两个节点的前一个。根据不同的需求,有时可能需要返回中间两个节点的后一个,此时可以在循环结束后将L向后移动一位。

进一步分析,为了确保快指针R可以安全地走两步,我们需要对链表长度为1和2的特殊情况进行处理。当链表只有一个节点时(即R->next == nullptr立即成立),我们直接返回L,因为此时链表中仅有的一个节点即为中间节点。

当链表有两个节点时,如果题目要求在有两个中间节点时返回第二个节点,那么初始时快指针R先向后走一步,此时如果R->next == nullptr,则按照题目要求返回L->next。 

考虑一个长度为3以及长度为4的链表作为例子。按照快慢指针的移动规则,初始时两者都位于头节点。快指针R首先向前移动两步,若移动一步后R->next=nullptr此时返回L,若移动两步后R->next=nullptr此时返回L->next,以上两种情况与其长度为1以及长度为2的链表返回情况相同。若链表长度为3,当快指针移动一步后,因为R->next不是nullptr,所以快指针继续移动,但在下一步R->next变为nullptr,此时根据规则返回L->next,即中间节点。对于长度为4的链表,快指针首次移动两步后,R->next不是nullptr,慢指针移动一步,此时快指针可以继续移动;快指针再次移动两步后,R变为nullptr,这时根据规则返回L->next,即第二个中间节点。以上两种情况与其长度为1以及长度为2的链表返回情况相同,为此我们使用while循环来解决此类问题。

通过上述分析,我们实际上将整个链表切分为由两个节点组成的小段(对于链表总长度为奇数的情况,最后会剩下一个单独的节点作为一个段),快指针R在每个小段的末尾检查是否可以继续前进,而慢指针L则稳步前进,最终准确地定位到链表的中间位置。

按照以上思路建立起来的模型, C++实现代码如下:

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* L=head;
        ListNode* R=head;
        while(R){
            if(R->next==nullptr)return L;
            R=R->next;
            if(R->next==nullptr)return L->next;
            R=R->next;
            L=L->next;
        }
          return L;
    }
};

 提交到 LeetCode 结果如下:

3.对于以上两类问题的总结

在解决链表中的两个不同问题——删除链表中倒数第n个节点和寻找链表中间节点时,我们都采用了双指针技术,但是具体的应用方式和目的存在一定的差异。以下是对这两种方法的综合分析,包括它们的相同点与不同点。

相同点

  1. 双指针的使用:两种方法都利用了双指针(一个快指针和一个慢指针)来高效地遍历链表,减少了时间复杂度。

  2. 单次遍历:通过合理设置指针的移动规则,两种方法都能够在单次遍历中达到目的,避免了多次遍历链表,提高了算法的效率。

  3. 指针的相对移动速度:在两个问题中,其核心主要是利用双指针不同的移动速度,这种不同速度的设置是为了在遍历过程中构建出特定的位置关系,从而实现各自的目标。

  4. 均考虑链表的临界情况:对于以上两个问题的分析以及代码实现,均是先考虑其普遍存在的情况,从而确定整体实现代码的思路。这种方法允许我们首先构建出一个基本的算法框架,确保它能够处理大多数标准情况。而后,我们根据链表的临界情况给出判断条件,这些临界情况包括但不限于链表长度为0(空链表)、1(只有一个节点的链表)、或者当要删除的节点正好是头节点等情况。通过这种方式,我们确保算法既能够处理一般情形,又能够妥善处理边缘或特殊情况。

不同点

  1. 目标不同

    • 删除倒数第n个节点的方法旨在通过双指针找到倒数第n+1个节点,以便删除其后的节点。
    • 寻找中间节点的方法则是利用快慢指针的速度差来当快指针到达链表末尾时,使慢指针正好位于中间节点。
  2. 指针初始化不同

    • 在删除倒数第n个节点时,两个指针都从头节点的前一个结点开始,并且右指针先单独向前移动n+1步,然后两个指针同时移动。
    • 在寻找中间节点时,快慢指针同样从头节点开始,但是它们从一开始就以不同的速度同时移动,直到快指针无法继续前进。
  3. 处理特殊情况的方式不同

    • 删除倒数第n个节点需要考虑特殊情况,如删除的是头节点,这时需要特别处理更新头节点。
    • 寻找中间节点的算法则需要考虑链表长度为奇数或偶数的情况,以及如何根据需求返回正确的中间节点(对于偶数长度的链表,可能需要决定是返回两个中间节点的前一个还是后一个)。
  4. 算法的目的和结果

    • 删除倒数第n个节点的方法关注于修改链表结构,通过改变指针引用来删除特定的节点。
    • 寻找中间节点的方法则专注于定位链表的中间位置,不涉及修改链表结构。
  • 21
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值