【力扣Leetcode题解系列之0019—Remove Nth Node From End of List 删除链表的倒数第 N 个结点】

19. Remove Nth Node From End of List 删除链表的倒数第 N 个结点:多语言实现与分析

一、题目分析

给定一个单链表,要求删除链表的倒数第 n 个节点,并返回链表的头节点。题目保证 n 是有效的。

二、常用解法

双指针法

  1. 思路
    • 引入哑节点:为了方便处理删除头节点的情况,创建一个哑节点(dummy node),让它的 next 指针指向链表的头节点。这样,在删除节点时,无论要删除的是头节点还是其他节点,处理方式都可以统一。
    • 快慢指针初始化:定义两个指针,快指针(fast)和慢指针(slow),同时让它们指向哑节点。
    • 快指针先行:快指针先向前移动 n 步,此时快慢指针之间的距离为 n
    • 同步移动:之后快慢指针同时移动,每次移动一步,直到快指针到达链表的末尾(即 fast->next == nullptrfast.next == None)。此时,慢指针正好指向倒数第 n + 1 个节点(因为快慢指针之间始终保持 n 的距离),而我们要删除的是倒数第 n 个节点,所以慢指针的下一个节点就是要删除的节点。
    • 删除节点:通过调整指针,将慢指针的 next 指针指向要删除节点的下一个节点,从而实现删除倒数第 n 个节点的操作。最后返回哑节点的 next 指针,即为新的链表头节点。
  2. 优点:这种方法通过一次遍历就可以找到并删除倒数第 n 个节点,时间复杂度为 (O(L)),其中 L 是链表的长度,相比于先遍历获取链表长度再进行删除的方法,效率更高。而且通过引入哑节点,统一了头节点和其他节点的删除操作,使代码更简洁和健壮。

三、多语言实现

Python实现

# Definition for singly-linked list.
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None


class Solution:
    def removeNthFromEnd(self, head, n):
        root = ListNode(0)
        root.next = head
        fast, slow, pre = root, root, root
        while n - 1:
            fast = fast.next
            n -= 1
        while fast.next:
            fast = fast.next
            pre = slow
            slow = slow.next
        pre.next = slow.next
        return root.next

Java实现

// Definition for singly-linked list.
class ListNode {
    int val;
    ListNode next;

    ListNode(int x) {
        val = x;
    }
}

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode fast = dummy;
        ListNode slow = dummy;
        ListNode pre = dummy;
        while (n > 0) {
            fast = fast.next;
            n--;
        }
        while (fast != null) {
            fast = fast.next;
            pre = slow;
            slow = slow.next;
        }
        pre.next = slow.next;
        return dummy.next;
    }
}

C实现

#include <stdio.h>
#include <stdlib.h>

// Definition for singly-linked list.
struct ListNode {
    int val;
    struct ListNode *next;
};

struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
    struct ListNode dummy;
    dummy.val = 0;
    dummy.next = head;
    struct ListNode *fast = &dummy;
    struct ListNode *slow = &dummy;
    struct ListNode *pre = &dummy;

    while (n > 0) {
        fast = fast->next;
        n--;
    }

    while (fast != NULL) {
        fast = fast->next;
        pre = slow;
        slow = slow->next;
    }

    pre->next = slow->next;
    return dummy.next;
}

C++实现

// Definition for singly-linked list.
struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        ListNode* prev = dummy;
        ListNode* cur = dummy;
        while (n--) {
            cur = cur->next;
        }
        while (cur && cur->next) {
            cur = cur->next;
            prev = prev->next;
        }
        prev->next = prev->next->next;
        return dummy->next;
    }
};

Go实现

package main

import "fmt"

// Definition for singly-linked list.
type ListNode struct {
    Val  int
    Next *ListNode
}

func removeNthFromEnd(head *ListNode, n int) *ListNode {
    dummy := &ListNode{Val: 0}
    dummy.Next = head
    fast, slow, pre := dummy, dummy, dummy
    for ; n > 0; n-- {
        fast = fast.Next
    }
    for fast != nil {
        fast = fast.Next
        pre = slow
        slow = slow.Next
    }
    pre.Next = slow.Next
    return dummy.Next
}

四、算法复杂性分析

时间复杂度

  1. 无论链表长度如何,快慢指针总共移动的次数最多为链表的长度 L。快指针先移动 n 步,然后快慢指针同步移动,直到快指针到达链表末尾,同步移动的次数为 L - n,所以总的时间复杂度为 (O(L)),其中 L 是链表的长度。

空间复杂度

  1. 除了链表本身占用的空间外,额外使用的空间为常数级,如定义的几个指针变量(哑节点指针、快慢指针等),所以空间复杂度为 (O(1))。

五、实现的关键点和难度

关键点

  1. 哑节点的使用:哑节点是解决删除头节点特殊情况的关键。通过引入哑节点,使得头节点和其他节点在删除操作上具有一致性,简化了代码逻辑,避免了额外的条件判断。
  2. 双指针的同步移动:准确控制快慢指针的移动逻辑是找到倒数第 n 个节点的核心。快指针先移动 n 步,然后快慢指针同步移动,确保快慢指针之间的距离始终为 n,从而在快指针到达链表末尾时,慢指针指向正确的位置。
  3. 指针的调整:在找到要删除节点的前一个节点(即慢指针的位置)后,正确调整指针,将其 next 指针指向要删除节点的下一个节点,完成删除操作。

难度

  1. 指针操作的准确性:链表操作中指针的指向和调整容易出错,需要对指针的概念和操作有清晰的理解。特别是在同步移动指针和调整指针以删除节点时,要确保每一步操作的正确性,否则可能导致链表结构混乱或内存泄漏等问题。
  2. 边界条件的处理:虽然题目保证 n 是有效的,但在实际编程中,对于链表为空等边界情况也需要考虑。尽管在本题中链表为空的情况未作要求,但良好的编程习惯要求我们在实现时考虑全面,以增强代码的健壮性。

六、扩展及难度加深题目

扩展题目1:删除链表的倒数第 k 到倒数第 m 个节点(k <= m

  1. 题目描述:给定一个链表和两个整数 km,要求删除链表中从倒数第 k 个节点到倒数第 m 个节点的所有节点,并返回链表的头节点。
  2. 解题思路:可以基于双指针法进行扩展。首先,快指针先移动 m 步,然后快慢指针同步移动,当快指针到达链表末尾时,慢指针指向倒数第 m + 1 个节点。然后再使用一个指针从慢指针开始,向前移动 m - k + 1 步,找到倒数第 k 个节点的前一个节点,调整指针完成删除操作。

扩展题目2:删除链表中所有值为特定值的倒数第 n 个节点

  1. 题目描述:给定一个链表、一个整数 n 和一个特定值 target,要求删除链表中所有值为 target 的节点中的倒数第 n 个节点,并返回链表的头节点。
  2. 解题思路:在双指针法的基础上,在移动指针的过程中,不仅要记录节点位置,还要判断节点值是否为 target。可以使用一个辅助数据结构(如栈)来记录值为 target 的节点,然后根据栈中的记录找到要删除的倒数第 n 个节点并进行删除。

难度加深题目1:在循环链表中删除倒数第 n 个节点

  1. 题目描述:给定一个循环链表和一个整数 n,要求删除循环链表中的倒数第 n 个节点,并返回链表的头节点。
  2. 解题思路:与单链表不同,循环链表没有明确的末尾。可以先使用快慢指针判断链表是否为循环链表(快指针每次移动两步,如果快指针能追上慢指针,则为循环链表)。然后,为了找到倒数第 n 个节点,可以先计算链表的长度(通过快慢指针同步移动并计数),再根据长度和 n 找到要删除节点的位置进行删除操作。

难度加深题目2:在双向链表中删除倒数第 n 个节点

  1. 题目描述:给定一个双向链表和一个整数 n,要求删除双向链表中的倒数第 n 个节点,并返回链表的头节点。
  2. 解题思路:双向链表有前驱指针和后继指针,在删除节点时可以更方便地调整指针。同样可以使用双指针法找到倒数第 n 个节点,然后通过调整前驱指针和后继指针完成删除操作。但要注意处理边界情况,如删除头节点和尾节点时,前驱指针和后继指针的调整方式与中间节点不同。

七、应用场合

  1. 链表数据结构的维护:在使用链表存储数据时,经常需要对链表中的节点进行删除操作。例如,在实现一个简单的内存管理系统中,链表用于表示内存块的分配情况,当某个内存块被释放时,就需要删除链表中对应的节点,本题的解法可以高效地完成这种删除操作。
  2. 算法设计中的子问题:在一些复杂的算法设计中,可能会涉及到对链表的特定节点删除操作。例如,在图算法中,有时会使用链表来表示图的邻接表,当需要更新图的结构时,可能需要删除链表中的某些节点,此时本题的双指针法思路可以作为算法设计的一部分。
  3. 数据处理与过滤:在数据处理过程中,如果数据以链表形式存储,并且需要根据某种条件删除特定位置的节点,如删除满足特定值条件的倒数第 n 个节点,就可以应用本题的方法进行处理,实现数据的过滤和整理。

扩展题目1:删除链表的倒数第 k 到倒数第 m 个节点(k <= m

  1. Python 实现
# Definition for singly-linked list.
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None


class Solution:
    def removeKtoMFromEnd(self, head, k, m):
        dummy = ListNode(0)
        dummy.next = head
        fast = dummy
        slow = dummy

        # 快指针先移动 m 步
        for _ in range(m):
            fast = fast.next

        # 快慢指针同步移动,直到快指针到达链表末尾
        while fast.next:
            fast = fast.next
            slow = slow.next

        # 找到倒数第 k 个节点的前一个节点
        temp = slow
        for _ in range(m - k):
            temp = temp.next

        # 删除倒数第 k 到倒数第 m 个节点
        slow.next = temp.next.next
        return dummy.next
  1. Java 实现
// Definition for singly-linked list.
class ListNode {
    int val;
    ListNode next;

    ListNode(int x) {
        val = x;
    }
}

class Solution {
    public ListNode removeKtoMFromEnd(ListNode head, int k, int m) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode fast = dummy;
        ListNode slow = dummy;

        // 快指针先移动 m 步
        for (int i = 0; i < m; i++) {
            fast = fast.next;
        }

        // 快慢指针同步移动,直到快指针到达链表末尾
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }

        // 找到倒数第 k 个节点的前一个节点
        ListNode temp = slow;
        for (int i = 0; i < m - k; i++) {
            temp = temp.next;
        }

        // 删除倒数第 k 到倒数第 m 个节点
        slow.next = temp.next.next;
        return dummy.next;
    }
}
  1. C++ 实现
// Definition for singly-linked list.
struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:
    ListNode* removeKtoMFromEnd(ListNode* head, int k, int m) {
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        ListNode* fast = dummy;
        ListNode* slow = dummy;

        // 快指针先移动 m 步
        for (int i = 0; i < m; i++) {
            fast = fast->next;
        }

        // 快慢指针同步移动,直到快指针到达链表末尾
        while (fast->next != nullptr) {
            fast = fast->next;
            slow = slow->next;
        }

        // 找到倒数第 k 个节点的前一个节点
        ListNode* temp = slow;
        for (int i = 0; i < m - k; i++) {
            temp = temp->next;
        }

        // 删除倒数第 k 到倒数第 m 个节点
        slow->next = temp->next->next;
        return dummy->next;
    }
};
  1. Go 实现
package main

import "fmt"

// Definition for singly-linked list.
type ListNode struct {
    Val  int
    Next *ListNode
}

func removeKtoMFromEnd(head *ListNode, k, m int) *ListNode {
    dummy := &ListNode{Val: 0}
    dummy.Next = head
    fast := dummy
    slow := dummy

    // 快指针先移动 m 步
    for i := 0; i < m; i++ {
        fast = fast.Next
    }

    // 快慢指针同步移动,直到快指针到达链表末尾
    for fast.Next != nil {
        fast = fast.Next
        slow = slow.Next
    }

    // 找到倒数第 k 个节点的前一个节点
    temp := slow
    for i := 0; i < m - k; i++ {
        temp = temp.Next
    }

    // 删除倒数第 k 到倒数第 m 个节点
    slow.Next = temp.Next.Next
    return dummy.Next
}

扩展题目2:删除链表中所有值为特定值的倒数第 n 个节点

  1. Python 实现
# Definition for singly-linked list.
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None


class Solution:
    def removeTargetNthFromEnd(self, head, n, target):
        dummy = ListNode(0)
        dummy.next = head
        stack = []
        cur = dummy

        while cur:
            if cur.val == target:
                stack.append(cur)
            cur = cur.next

        if len(stack) < n:
            return dummy.next

        node_to_delete = stack[-n]
        prev = dummy
        while prev.next != node_to_delete:
            prev = prev.next

        prev.next = node_to_delete.next
        return dummy.next
  1. Java 实现
// Definition for singly-linked list.
class ListNode {
    int val;
    ListNode next;

    ListNode(int x) {
        val = x;
    }
}

import java.util.Stack;

class Solution {
    public ListNode removeTargetNthFromEnd(ListNode head, int n, int target) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        Stack<ListNode> stack = new Stack<>();
        ListNode cur = dummy;

        while (cur != null) {
            if (cur.val == target) {
                stack.push(cur);
            }
            cur = cur.next;
        }

        if (stack.size() < n) {
            return dummy.next;
        }

        ListNode nodeToDelete = stack.elementAt(stack.size() - n);
        ListNode prev = dummy;
        while (prev.next != nodeToDelete) {
            prev = prev.next;
        }

        prev.next = nodeToDelete.next;
        return dummy.next;
    }
}
  1. C++ 实现
// Definition for singly-linked list.
struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

#include <stack>

class Solution {
public:
    ListNode* removeTargetNthFromEnd(ListNode* head, int n, int target) {
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        std::stack<ListNode*> stack;
        ListNode* cur = dummy;

        while (cur != nullptr) {
            if (cur->val == target) {
                stack.push(cur);
            }
            cur = cur->next;
        }

        if (stack.size() < n) {
            return dummy->next;
        }

        ListNode* nodeToDelete = stack.top();
        for (int i = 1; i < n; i++) {
            stack.pop();
            nodeToDelete = stack.top();
        }

        ListNode* prev = dummy;
        while (prev->next != nodeToDelete) {
            prev = prev->next;
        }

        prev->next = nodeToDelete->next;
        return dummy->next;
    }
};
  1. Go 实现
package main

import (
    "fmt"
)

// Definition for singly-linked list.
type ListNode struct {
    Val  int
    Next *ListNode
}

func removeTargetNthFromEnd(head *ListNode, n, target int) *ListNode {
    dummy := &ListNode{Val: 0}
    dummy.Next = head
    stack := []*ListNode{}
    cur := dummy

    for cur != nil {
        if cur.Val == target {
            stack = append(stack, cur)
        }
        cur = cur.Next
    }

    if len(stack) < n {
        return dummy.Next
    }

    nodeToDelete := stack[len(stack) - n]
    prev := dummy
    for prev.Next != nodeToDelete {
        prev = prev.Next
    }

    prev.Next = nodeToDelete.Next
    return dummy.Next
}

难度加深题目1:在循环链表中删除倒数第 n 个节点

  1. Python 实现
# Definition for singly-linked list.
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None


class Solution:
    def removeNthFromEndInCircular(self, head, n):
        slow = head
        fast = head
        has_cycle = False

        # 判断是否为循环链表
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            if slow == fast:
                has_cycle = True
                break

        if not has_cycle:
            return head

        # 计算链表长度
        length = 1
        cur = slow
        while cur.next != slow:
            length += 1
            cur = cur.next

        # 找到要删除节点的位置
        position = (length - n) % length
        cur = head
        for _ in range(position - 1):
            cur = cur.next

        # 删除节点
        if position == 0:
            while cur.next != head:
                cur = cur.next
            cur.next = head.next
            return cur.next
        else:
            cur.next = cur.next.next
            return head
  1. Java 实现
// Definition for singly-linked list.
class ListNode {
    int val;
    ListNode next;

    ListNode(int x) {
        val = x;
    }
}

class Solution {
    public ListNode removeNthFromEndInCircular(ListNode head, int n) {
        ListNode slow = head;
        ListNode fast = head;
        boolean hasCycle = false;

        // 判断是否为循环链表
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                hasCycle = true;
                break;
            }
        }

        if (!hasCycle) {
            return head;
        }

        // 计算链表长度
        int length = 1;
        ListNode cur = slow;
        while (cur.next != slow) {
            length += 1;
            cur = cur.next;
        }

        // 找到要删除节点的位置
        int position = (length - n) % length;
        cur = head;
        for (int i = 0; i < position - 1; i++) {
            cur = cur.next;
        }

        // 删除节点
        if (position == 0) {
            while (cur.next != head) {
                cur = cur.next;
            }
            cur.next = head.next;
            return cur.next;
        } else {
            cur.next = cur.next.next;
            return head;
        }
    }
}
  1. C++ 实现
// Definition for singly-linked list.
struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:
    ListNode* removeNthFromEndInCircular(ListNode* head, int n) {
        ListNode* slow = head;
        ListNode* fast = head;
        bool hasCycle = false;

        // 判断是否为循环链表
        while (fast != nullptr && fast->next != nullptr) {
            slow = slow->next;
            fast = fast->next->next;
            if (slow == fast) {
                hasCycle = true;
                break;
            }
        }

        if (!hasCycle) {
            return head;
        }

        // 计算链表长度
        int length = 1;
        ListNode* cur = slow;
        while (cur->next != slow) {
            length += 1;
            cur = cur->next;
        }

        // 找到要删除节点的位置
        int position = (length - n) % length;
        cur = head;
        for (int i = 0; i < position - 1; i++) {
            cur = cur->next;
        }

        // 删除节点
        if (position == 0) {
            while (cur->next != head) {
                cur = cur->next;
            }
            cur->next = head->next;
            return cur->next;
        } else {
            cur->next = cur->next->next;
            return head;
        }
    }
};
  1. Go 实现
package main

import (
    "fmt"
)

// Definition for singly-linked list.
type ListNode struct {
    Val  int
    Next *ListNode
}

func removeNthFromEndInCircular(head *ListNode, n int) *ListNode {
    slow := head
    fast := head
    hasCycle := false

    // 判断是否为循环链表
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
        if slow == fast {
            hasCycle = true
            break
        }
    }

    if!hasCycle {
        return head
    }

    // 计算链表长度
    length := 1
    cur := slow
    for cur.Next != slow {
        length++
        cur = cur.Next
    }

    // 找到要删除节点的位置
    position := (length - n) % length
    cur = head
    for i := 0; i < position - 1; i++ {
        cur = cur.Next
    }

    // 删除节点
    if position == 0 {
        for cur.Next != head {
            cur = cur.Next
        }
        cur.Next = head.Next
        return cur.Next
    } else {
        cur.Next = cur.Next.Next
        return head
    }
}

难度加深题目2:在双向链表中删除倒数第 n 个节点

  1. Python 实现
# Definition for a double-linked list.
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None
        self.prev = None


class Solution:
    def removeNthFromEndInDouble(self, head, n):
        dummy = ListNode(0)
        dummy.next = head
        if head:
            head.prev = dummy

        fast = dummy
        slow = dummy

        while n:
            fast = fast.next
            n -= 1

        while fast.next:
            fast = fast.next
            slow = slow.next

        if slow.next:
            slow.next.prev = slow.prev
            slow.prev.next = slow.next
        return dummy.next
  1. Java 实现
// Definition for a double-linked list.
class ListNode {
    int val;
    ListNode next;
    ListNode prev;

    ListNode(int x) {
        val = x;
    }
}

class Solution {
    public ListNode removeNthFromEndInDouble(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        if (head != null) {
            head.prev = dummy;
        }

        ListNode fast = dummy;
        ListNode slow = dummy;

        while (n > 0) {
            fast = fast.next;
            n--;
        }

        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }

        if (slow.next != null) {
            slow.next.prev = slow.prev;
            slow.prev.next = slow.next;
        }
        return dummy.next;
    }
}
  1. C++ 实现
// Definition for a double-linked list.
struct ListNode {
    int val;
    ListNode *next;
    ListNode *prev;
    ListNode(int x) : val(x), next(NULL), prev(NULL) {}
};

class Solution {
public:
    ListNode* removeNthFromEndInDouble(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        if (head) {
            head->prev = dummy;
        }

        ListNode* fast = dummy;
        ListNode* slow = dummy;

        while (n > 0) {
            fast = fast->next;
            n--;
        }

        while (fast->next != nullptr) {
            fast = fast->next;
            slow = slow->next;
        }

        if (slow->next != nullptr) {
            slow->next->prev = slow->        slow->prev->next = slow->next;
        }
        return dummy->next;
    }
};
  1. Go实现
package main

import "fmt"

// Definition for a double - linked list.
type ListNode struct {
    Val   int
    Next  *ListNode
    Prev  *ListNode
}

func removeNthFromEndInDouble(head *ListNode, n int) *ListNode {
    dummy := &ListNode{Val: 0}
    dummy.Next = head
    if head != nil {
        head.Prev = dummy
    }

    fast := dummy
    slow := dummy

    for ; n > 0; n-- {
        fast = fast.Next
    }

    for fast.Next != nil {
        fast = fast.Next
        slow = slow.Next
    }

    if slow.Next != nil {
        slow.Next.Prev = slow.Prev
        slow.Prev.Next = slow.Next
    }
    return dummy.Next
}

算法复杂性分析 - 扩展题目1

  1. 时间复杂度:快指针先移动 m 步,时间复杂度为 (O(m)),然后快慢指针同步移动,移动次数最多为链表长度 L,时间复杂度为 (O(L)),最后找到倒数第 k 个节点前一个节点移动 (m - k) 步,时间复杂度为 (O(m - k))。所以总的时间复杂度为 (O(m + L + m - k) = O(L + 2m - k)),由于 (k \leq m),可简化为 (O(L + m)),其中 L 是链表长度。
  2. 空间复杂度:除了链表本身,额外使用的空间为常数级,如定义的指针变量,空间复杂度为 (O(1))。

算法复杂性分析 - 扩展题目2

  1. 时间复杂度:遍历链表将值为 target 的节点入栈,时间复杂度为 (O(L)),其中 L 是链表长度。之后在栈中找到要删除节点及其前驱节点,时间复杂度为 (O(s)),其中 s 是栈的大小(即值为 target 的节点个数),由于 (s \leq L),所以总的时间复杂度为 (O(L))。
  2. 空间复杂度:使用栈来存储值为 target 的节点,在最坏情况下,栈的大小为链表长度 L,所以空间复杂度为 (O(L))。

算法复杂性分析 - 难度加深题目1

  1. 时间复杂度:判断是否为循环链表,快慢指针移动最多 (O(L)) 次。计算链表长度,需要遍历循环链表一周,时间复杂度为 (O(L))。找到要删除节点位置及删除节点操作,时间复杂度为 (O(L))。所以总的时间复杂度为 (O(L))。
  2. 空间复杂度:除了链表本身,额外使用的空间为常数级,如定义的指针变量,空间复杂度为 (O(1))。

算法复杂性分析 - 难度加深题目2

  1. 时间复杂度:初始化哑节点及调整头节点前驱指针,时间复杂度为 (O(1))。快慢指针移动找到要删除节点位置,时间复杂度为 (O(L)),其中 L 是链表长度。删除节点时调整指针,时间复杂度为 (O(1))。所以总的时间复杂度为 (O(L))。
  2. 空间复杂度:除了链表本身,额外使用的空间为常数级,如定义的指针变量,空间复杂度为 (O(1))。

实现的关键点和难度 - 扩展题目1

  1. 关键点:与原问题类似,要准确控制快慢指针的移动逻辑。快指针先移动 m 步,之后快慢指针同步移动找到倒数第 m + 1 个节点。然后通过额外的指针移动找到倒数第 k 个节点的前一个节点,从而正确删除倒数第 k 到倒数第 m 个节点。
  2. 难度:相比原问题,增加了对 km 两个参数的处理,需要更细致地控制指针移动的步数和条件,以确保删除操作的正确性。同时,在处理过程中要清晰地理解链表结构的变化,避免误操作导致链表断裂或产生其他错误。

实现的关键点和难度 - 扩展题目2

  1. 关键点:使用栈来记录值为 target 的节点,以便后续找到倒数第 n 个节点。在遍历链表时准确判断节点值,并将符合条件的节点入栈。在栈中定位要删除节点及其前驱节点,并正确调整链表指针完成删除操作。
  2. 难度:引入栈增加了空间复杂度,同时需要在遍历链表和操作栈之间进行协调。此外,要处理栈中元素不足 n 的情况,确保算法的鲁棒性。对链表指针的调整也需要格外小心,因为涉及到多个指针的操作,容易出现逻辑错误。

实现的关键点和难度 - 难度加深题目1

  1. 关键点:首先要通过快慢指针判断链表是否为循环链表。然后计算链表长度,这是找到要删除节点位置的关键。在找到位置后,针对循环链表的特点,特别是删除头节点的情况,正确调整指针完成删除操作。
  2. 难度:处理循环链表比普通单链表复杂,需要考虑循环的特性。判断循环、计算长度以及删除节点时的指针调整都需要仔细处理,任何一步出错都可能导致程序出现逻辑错误或死循环。例如,在删除头节点时,需要遍历到链表最后一个节点来调整指针,这需要对循环链表的结构有深入理解。

实现的关键点和难度 - 难度加深题目2

  1. 关键点:双向链表有前驱指针和后继指针,在删除节点时需要同时调整这两个指针。利用双指针法找到倒数第 n 个节点,然后正确调整前驱指针和后继指针,确保链表结构的完整性。
  2. 难度:相比单链表,双向链表的指针操作更复杂,需要同时关注前驱和后继指针的变化。在处理边界情况(如删除头节点和尾节点)时,前驱指针和后继指针的调整方式与中间节点不同,需要分别进行处理,这增加了代码的复杂性和出错的可能性。

应用场合

  1. 数据库索引维护:在数据库中,某些索引结构可能采用链表形式存储。当数据发生变化(如删除特定记录)时,可能需要删除链表中相应的节点。例如,在一个基于链表实现的倒排索引中,如果某个文档从索引中移除,就需要删除链表中对应文档的节点,扩展题目中的方法可以帮助高效地完成这种操作。
  2. 操作系统内存管理:操作系统的内存管理模块中,链表常用于管理内存块。当释放特定条件下的内存块(如释放最近最少使用的内存块的倒数第 n 个)时,可以应用这些方法来删除链表中对应的节点,以实现内存的有效回收和管理。
  3. 图算法中的边删除:在图算法中,有时用链表来表示图的边集。如果需要删除图中满足特定条件的边(如删除连接特定顶点的倒数第 n 条边),可以将边集看作链表,利用上述方法进行删除操作,从而实现图结构的动态更新。

进一步思考与总结

  1. 多种解法的对比与选择
    • 在解决链表相关问题时,双指针法是一种非常有效的策略,它通过巧妙地利用两个指针的移动来遍历链表,从而在一次遍历中完成复杂的任务,如本题中删除倒数第 n 个节点。在扩展题目和难度加深题目中,双指针法也通过不同的变形和结合其他数据结构(如栈)来应对更复杂的需求。
    • 对于不同的链表操作需求,选择合适的解法至关重要。例如,在删除链表的倒数第 k 到倒数第 m 个节点的问题中,双指针法通过调整指针移动的步数和逻辑来实现;而在删除链表中所有值为特定值的倒数第 n 个节点的问题中,结合栈结构来记录特定值节点,然后利用双指针的思想找到并删除目标节点。
    • 在实际应用中,需要根据具体的问题场景、数据规模以及对时间和空间复杂度的要求来选择最合适的解法。如果对空间复杂度要求较高,应尽量避免使用像扩展题目2中栈这种额外空间需求较大的方法;如果链表长度较长且对时间复杂度要求苛刻,应优先选择时间复杂度较低的算法。
  2. 链表问题的通用解题思路
    • 理解链表结构:深入理解链表的结构特点是解决链表问题的基础。无论是单链表、双向链表还是循环链表,每个节点的指针指向关系决定了链表的遍历方式和操作方法。例如,双向链表可以通过前驱和后继指针方便地进行双向遍历和节点删除操作;循环链表则需要特别注意循环的起始和结束条件,避免出现死循环。
    • 确定指针移动逻辑:在大多数链表问题中,指针的移动逻辑是解题的核心。通过分析问题的要求,确定指针的初始位置、移动条件和移动步数。如在删除倒数第 n 个节点的问题中,快指针先移动 n 步,然后快慢指针同步移动,这种移动逻辑是基于倒数位置的定义和链表的遍历特性确定的。
    • 处理边界条件:链表问题中边界条件的处理至关重要。常见的边界条件包括链表为空、只有一个节点、删除头节点、删除尾节点等。在实现算法时,必须确保在这些边界条件下算法的正确性和鲁棒性。例如,通过引入哑节点来统一头节点和其他节点的删除操作,避免了针对头节点删除的特殊处理,使代码更加简洁和通用。
    • 结合其他数据结构:对于一些复杂的链表问题,单纯依靠链表本身的操作可能无法高效解决,需要结合其他数据结构。如扩展题目2中使用栈来记录特定值的节点,从而方便地找到并删除目标节点。此外,还可以结合哈希表来快速查找链表中的节点,提高算法效率。
  3. 与其他数据结构的联系
    • 链表与数组:链表和数组是两种基本的数据结构,它们在存储方式和操作特性上有很大的不同。链表适合动态插入和删除操作,而数组适合随机访问。在解决某些问题时,可以根据具体需求将链表转换为数组(如通过遍历链表将节点值存入数组),利用数组的特性进行处理,然后再将结果转换回链表。例如,在对链表中的元素进行排序时,可以先将链表元素存入数组,使用高效的排序算法(如快速排序、归并排序)对数组进行排序,然后再将排序后的数组元素重新构建为链表。
    • 链表与栈和队列:栈和队列可以用链表来实现。例如,用链表实现栈时,链表的头节点作为栈顶,插入和删除操作都在头节点进行,时间复杂度为 (O(1));用链表实现队列时,链表的头节点作为队头,尾节点作为队尾,插入操作在尾节点进行,删除操作在头节点进行,时间复杂度也为 (O(1))。在解决链表问题时,也可以借助栈和队列的特性。如在扩展题目2中使用栈来记录特定值的节点,这是利用了栈的后进先出特性。
  4. 学习建议
    • 多做练习:链表问题是算法学习中的重要部分,通过大量的练习可以熟悉各种链表操作和解题技巧。可以从简单的链表遍历、插入、删除操作开始,逐渐过渡到复杂的问题,如在循环链表或双向链表中进行特定操作。
    • 分析时间和空间复杂度:在完成每一道链表问题后,分析算法的时间和空间复杂度,理解不同解法在复杂度上的差异,从而学会根据实际需求选择最优解法。这有助于提高对算法性能的理解和优化能力。
    • 画图辅助理解:链表是一种可视化较强的数据结构,在解决问题时,通过画图来表示链表的结构和指针的移动过程,可以帮助更好地理解问题和算法逻辑,尤其是在处理复杂的链表操作时,画图能够直观地展示链表的变化,减少出错的可能性。
    • 总结归纳:将相似的链表问题进行总结归纳,分析它们的共性和差异,提炼出通用的解题思路和技巧。例如,对于各种删除链表特定节点的问题,可以总结出双指针法的不同应用方式,以及如何处理边界条件和结合其他数据结构。这样在遇到新的链表问题时,能够更快地找到解题方向。

通过对删除链表倒数第 n 个节点及其扩展和难度加深题目的分析与实现,我们不仅掌握了链表操作的具体方法,还深入理解了算法设计、复杂度分析以及数据结构之间的联系。这对于提升算法能力和解决实际问题的能力具有重要意义。

<think>嗯,用户想了解如何删除链表倒数第N个节点。这道题我之前做过,但得仔细想想步骤。首先,链表的问题通常需要双指针技巧,尤其是处理倒数节点的时候。比如,要删除倒数第N个节点,可能需要找到它的前驱节点,然后调整指针。 那具体怎么做呢?比如,假设有一个链表是1->2->3->4->5,要删除倒数第2个节点,也就是4,结果应该是1->2->3->5。这时候,常规思路是先遍历一次得到长度,再计算正数的位置,然后第二次遍历到那个位置删除。但这样需要两次遍历,有没有更高效的方法? 对了,可以用快慢双指针。让快指针先走N步,然后快慢一起移动,直到快指针到末尾。这时慢指针就指向要删除节点的前一个节点。比如例子中,快指针先走2步到2,然后快慢一起移动,当快指针到5的时候,慢指针在3的位置,这样就可以把3的next指向5,从而跳过4。 但需要注意边界情况,比如如果删除的是头节点怎么办?比如链表是1->2,N=2,那么删除的是头节点。这时候需要处理头节点的情况,或者在前面加一个哑节点(dummy node),这样所有节点都有前驱,方便统一处理。比如哑节点指向头节点,然后快慢指针从哑节点开始,这样当快指针走完,慢指针就指向要删除节点的前驱,即使删除的是头节点,也能正确操作。 步骤大概是:创建哑节点,快指针先走N+1步(因为要找到前驱),然后快慢同时移动,直到快指针为null,此时慢指针的下一个节点就是需要删除的,执行删除操作。最后返回哑节点的next作为新头节点。 需要验证这个思路的正确性。比如例子中的情况,快指针先走N+1步的话,可能快指针会走到null,这时候慢指针的next就是需要删除的节点。比如当链表长度等于N时,删除头节点的情况,这时候快指针移动N+1步会超出,此时慢指针的下一个节点就是头节点,删除即可。 另外,要确保输入的N是有效的,比如不大于链表长度。题目可能假设N是有效的,所以不需要处理这种情况。 然后,代码实现的话,需要注意指针的移动次数。比如,初始化哑节点,然后快慢指针都指向哑节点。快指针先移动N+1次,然后同步移动直到快指针为null。这样慢指针的位置就是前驱节点。 比如,链表长度是L,快指针走了N+1步,然后剩下的步数是L - (N+1),这时候慢指针走了L - (N+1)步,所以慢指针的位置是第 (L - N -1) 个节点,而倒数第N个节点是第L - N +1 个节点,所以前驱节点是第L - N个节点,也就是L - N的位置。这时候,用L - (N+1)步走到前驱的话,可能要看具体的例子。 比如链表长度是5,N=2,那么L - N =3,也就是第3个节点是前驱。而用上述方法,快指针走3步(N+1=3),然后快慢一起移动直到快指针到末尾。初始时快指针在dummy,走3步到节点3的位置。然后快指针还有2步走到末尾(节点5之后是null)。这时候慢指针从dummy开始,移动2步到节点3的位置,此时它的next是4,也就是要删除的节点。这时候删除即可。 这似乎是对的。那代码的结构大概是: 创建dummy节点,dummy.next = head; 快慢指针都指向dummy; 快指针先移动N+1次; 然后同时移动直到快指针为null; 此时慢指针的next是要删除的节点,执行slow.next = slow.next.next; 返回dummy.next; 这样的话,可以处理各种情况,包括删除头节点的情况。 总结起来,步骤是: 1. 添加哑节点,简化头节点删除的情况。 2. 快指针先移动N+1步。 3. 快慢指针同时移动直到快指针到末尾。 4. 删除慢指针的下一个节点。 5. 返回哑节点的下一个节点作为新头。 这样时间复杂度是O(L),L是链表长度,空间复杂度是O(1)。</think>### 删除链表倒数第 N 个结点 | 力扣题解 #### 问题分析 题目要求删除链表的**倒数第 N 个结点**,并返回头结点。核心难点在于如何高效定位倒数第 N 个结点,并处理边界情况(如删除结点)。 --- #### 解决思路 **核心技巧:双指针法(快慢指针)** 1. **哑结点(Dummy Node)**:在链表头部前添加一个虚拟结点,用于统一处理删除结点的情况。 2. **快指针先移动 N+1 步**:确保当快指针遍历到链表末尾时,慢指针刚好指向倒数第 N 个结点的**前驱结点**。 3. **同步移动双指针**:直到快指针指向 `null`,此时慢指针的 `next` 结点即为要删除结点。 4. **删除目标结点**:修改指针跳过目标结点。 --- #### 分步解析 1. **初始化哑结点和双指针** ```python dummy = ListNode(0) # 哑结点 dummy.next = head fast = slow = dummy # 双指针均从哑结点出发 ``` 2. **快指针先移动 N+1 步** ```python for _ in range(n + 1): fast = fast.next ``` *示例*:链表 `1->2->3->4->5`,`n=2`,快指针移动后指向 `3`。 3. **同步移动双指针** ```python while fast: fast = fast.next slow = slow.next ``` *关键点*:当快指针到达末尾时,慢指针指向倒数第 N+1 个结点(即目标结点的前驱)。 4. **删除目标结点** ```python slow.next = slow.next.next # 跳过倒数第 N 个结点 ``` 5. **返回新链表结点** ```python return dummy.next ``` --- #### 完整代码(Python) ```python class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def removeNthFromEnd(head: ListNode, n: int) -> ListNode: dummy = ListNode(0) dummy.next = head fast = slow = dummy # 快指针先走 n+1 步 for _ in range(n + 1): fast = fast.next # 同步移动直到快指针到末尾 while fast: fast = fast.next slow = slow.next # 删除目标结点 slow.next = slow.next.next return dummy.next ``` --- #### 复杂度分析 - **时间复杂度**:$$O(L)$$,其中 $$L$$ 为链表长度。仅需一次遍历。 - **空间复杂度**:$$O(1)$$,仅使用常数级额外空间。 --- #### 边界案例验证 1. **删除结点** 输入:链表 `[1,2]`,`n=2` 输出:`[2]` 处理过程:哑结点帮助统一操作,慢指针最终指向哑结点,直接跳过原头结点。 2. **删除中间结点** 输入:链表 `[1->2->3->4->5]`,`n=2` 输出:`[1->2->3->5]` 处理过程:慢指针指向 `3`,删除 `4`。 3. **删除结点** 输入:链表 `[1->2]`,`n=1` 输出:`[1]` 处理过程:慢指针指向 `1`,跳过 `2`。 --- #### 总结 通过**双指针法**和**哑结点技巧**,可以高效且简洁地解决链表倒数结点删除问题。关键在于通过快指针的提前移动,保证慢指针精确定位到目标前驱结点
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值