Files
interview/16-LeetCode Hot 100/删除链表的倒数第N个结点.md

16 KiB
Raw Blame History

删除链表的倒数第N个结点 (Remove Nth Node From End of List)

题目描述

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

示例

示例 1

输入head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2

输入head = [1], n = 1
输出:[]

示例 3

输入head = [1,2], n = 1
输出:[1]

约束条件

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

进阶

你能尝试使用一趟扫描实现吗?

解题思路

方法一:双指针法(推荐)

**核心思想:**使用两个指针 fastslowfast 先移动 n 步,然后 fastslow 一起移动,直到 fast 到达链表末尾。此时 slow 指向要删除结点的前一个结点。

算法步骤:

  1. 创建哑结点 dummy,指向链表头
  2. 初始化 fastslow 指针都指向 dummy
  3. fast 先移动 n + 1
  4. fastslow 同时移动,直到 fastnil
  5. 此时 slow.next 就是要删除的结点,执行 slow.next = slow.next.next
  6. 返回 dummy.next

为什么移动 n + 1 步?

  • 这样 slow 最终会停在要删除结点的前一个结点
  • 方便删除操作:slow.next = slow.next.next

方法二:计算长度法

**核心思想:**先遍历链表计算长度,然后计算要删除的正数位置,再遍历到该位置删除结点。

算法步骤:

  1. 遍历链表,计算长度 length
  2. 要删除的正数位置为 length - n
  3. 创建哑结点 dummy,指向链表头
  4. 遍历到第 length - n - 1 个结点
  5. 删除下一个结点
  6. 返回 dummy.next

方法三:栈法

**核心思想:**将所有结点压入栈中,然后弹出 n 个结点,栈顶就是要删除结点的前一个结点。

算法步骤:

  1. 创建哑结点 dummy
  2. 将所有结点压入栈
  3. 弹出 n 个结点
  4. 栈顶结点的 next 指向要删除结点的 next
  5. 返回 dummy.next

代码实现

Go 实现(双指针法)

package main

import "fmt"

// ListNode 链表结点定义
type ListNode struct {
	Val  int
	Next *ListNode
}

func removeNthFromEnd(head *ListNode, n int) *ListNode {
	// 创建哑结点,处理删除头结点的特殊情况
	dummy := &ListNode{0, head}
	fast, slow := dummy, dummy

	// fast 先移动 n + 1 步
	for i := 0; i <= n; i++ {
		fast = fast.Next
	}

	// fast 和 slow 一起移动,直到 fast 为 nil
	for fast != nil {
		fast = fast.Next
		slow = slow.Next
	}

	// 删除 slow 的下一个结点
	slow.Next = slow.Next.Next

	return dummy.Next
}

// 辅助函数:创建链表
func createList(nums []int) *ListNode {
	dummy := &ListNode{}
	current := dummy
	for _, num := range nums {
		current.Next = &ListNode{num, nil}
		current = current.Next
	}
	return dummy.Next
}

// 辅助函数:打印链表
func printList(head *ListNode) {
	current := head
	for current != nil {
		fmt.Printf("%d", current.Val)
		if current.Next != nil {
			fmt.Printf(" -> ")
		}
		current = current.Next
	}
	fmt.Println()
}

// 测试用例
func main() {
	// 测试用例1
	head1 := createList([]int{1, 2, 3, 4, 5})
	fmt.Print("输入: ")
	printList(head1)
	fmt.Printf("n = 2\n")
	result1 := removeNthFromEnd(head1, 2)
	fmt.Print("输出: ")
	printList(result1)

	// 测试用例2: 删除头结点
	head2 := createList([]int{1})
	fmt.Print("\n输入: ")
	printList(head2)
	fmt.Printf("n = 1\n")
	result2 := removeNthFromEnd(head2, 1)
	fmt.Print("输出: ")
	printList(result2)

	// 测试用例3: 删除最后一个结点
	head3 := createList([]int{1, 2})
	fmt.Print("\n输入: ")
	printList(head3)
	fmt.Printf("n = 1\n")
	result3 := removeNthFromEnd(head3, 1)
	fmt.Print("输出: ")
	printList(result3)

	// 测试用例4: 长链表
	head4 := createList([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
	fmt.Print("\n输入: ")
	printList(head4)
	fmt.Printf("n = 5\n")
	result4 := removeNthFromEnd(head4, 5)
	fmt.Print("输出: ")
	printList(result4)
}

Java 实现(双指针法)

public class RemoveNthFromEnd {

    // 链表结点定义
    public static class ListNode {
        int val;
        ListNode next;
        ListNode() {}
        ListNode(int val) { this.val = val; }
        ListNode(int val, ListNode next) { this.val = val; this.next = next; }
    }

    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 创建哑结点,处理删除头结点的特殊情况
        ListNode dummy = new ListNode(0, head);
        ListNode fast = dummy;
        ListNode slow = dummy;

        // fast 先移动 n + 1 步
        for (int i = 0; i <= n; i++) {
            fast = fast.next;
        }

        // fast 和 slow 一起移动,直到 fast 为 null
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }

        // 删除 slow 的下一个结点
        slow.next = slow.next.next;

        return dummy.next;
    }

    // 辅助函数:创建链表
    private ListNode createList(int[] nums) {
        ListNode dummy = new ListNode();
        ListNode current = dummy;
        for (int num : nums) {
            current.next = new ListNode(num);
            current = current.next;
        }
        return dummy.next;
    }

    // 辅助函数:打印链表
    private void printList(ListNode head) {
        ListNode current = head;
        while (current != null) {
            System.out.print(current.val);
            if (current.next != null) {
                System.out.print(" -> ");
            }
            current = current.next;
        }
        System.out.println();
    }

    // 测试用例
    public static void main(String[] args) {
        RemoveNthFromEnd solution = new RemoveNthFromEnd();

        // 测试用例1
        ListNode head1 = solution.createList(new int[]{1, 2, 3, 4, 5});
        System.out.print("输入: ");
        solution.printList(head1);
        System.out.println("n = 2");
        ListNode result1 = solution.removeNthFromEnd(head1, 2);
        System.out.print("输出: ");
        solution.printList(result1);

        // 测试用例2: 删除头结点
        ListNode head2 = solution.createList(new int[]{1});
        System.out.print("\n输入: ");
        solution.printList(head2);
        System.out.println("n = 1");
        ListNode result2 = solution.removeNthFromEnd(head2, 1);
        System.out.print("输出: ");
        solution.printList(result2);

        // 测试用例3: 删除最后一个结点
        ListNode head3 = solution.createList(new int[]{1, 2});
        System.out.print("\n输入: ");
        solution.printList(head3);
        System.out.println("n = 1");
        ListNode result3 = solution.removeNthFromEnd(head3, 1);
        System.out.print("输出: ");
        solution.printList(result3);

        // 测试用例4: 长链表
        ListNode head4 = solution.createList(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
        System.out.print("\n输入: ");
        solution.printList(head4);
        System.out.println("n = 5");
        ListNode result4 = solution.removeNthFromEnd(head4, 5);
        System.out.print("输出: ");
        solution.printList(result4);
    }
}

Go 实现(计算长度法)

func removeNthFromEndByLength(head *ListNode, n int) *ListNode {
	if head == nil {
		return nil
	}

	// 计算链表长度
	length := 0
	current := head
	for current != nil {
		length++
		current = current.Next
	}

	// 要删除的正数位置
	pos := length - n

	// 创建哑结点
	dummy := &ListNode{0, head}
	current = dummy

	// 移动到要删除结点的前一个结点
	for i := 0; i < pos; i++ {
		current = current.Next
	}

	// 删除结点
	current.Next = current.Next.Next

	return dummy.Next
}

Java 实现(栈法)

import java.util.Stack;

public ListNode removeNthFromEndByStack(ListNode head, int n) {
    // 创建哑结点
    ListNode dummy = new ListNode(0, head);

    // 将所有结点压入栈
    Stack<ListNode> stack = new Stack<>();
    ListNode current = dummy;
    while (current != null) {
        stack.push(current);
        current = current.next;
    }

    // 弹出 n 个结点
    for (int i = 0; i < n; i++) {
        stack.pop();
    }

    // 栈顶就是要删除结点的前一个结点
    ListNode prev = stack.peek();
    prev.next = prev.next.next;

    return dummy.next;
}

复杂度分析

双指针法

  • 时间复杂度: O(L)

    • 其中 L 是链表长度
    • 只需遍历链表一次
  • 空间复杂度: O(1)

    • 只使用了常数级别的额外空间
    • 只需要几个指针变量

计算长度法

  • 时间复杂度: O(L)

    • 第一次遍历计算长度O(L)
    • 第二次遍历删除结点O(L)
    • 总时间复杂度O(2L) = O(L)
  • 空间复杂度: O(1)

    • 只使用了常数级别的额外空间

栈法

  • 时间复杂度: O(L)

    • 需要遍历链表一次
  • 空间复杂度: O(L)

    • 需要额外的栈空间存储所有结点

进阶问题

Q1: 如果链表是循环链表,应该如何处理?

A: 需要先判断是否为循环链表,如果是,需要找到尾结点并断开循环。

func removeNthFromEndCircular(head *ListNode, n int) *ListNode {
    if head == nil {
        return nil
    }

    // 检测是否有环
    hasCycle := detectCycle(head)
    if !hasCycle {
        return removeNthFromEnd(head, n)
    }

    // 如果有环,需要先找到环的入口和长度
    // 然后根据 n 的值决定如何删除
    // 这是一个复杂的问题,需要更多边界条件处理

    return head
}

func detectCycle(head *ListNode) bool {
    slow, fast := head, head
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
        if slow == fast {
            return true
        }
    }
    return false
}

Q2: 如果要求删除前 n 个结点,应该如何修改?

A: 直接遍历到第 n-1 个结点,然后删除后续所有结点。

func removeFirstN(head *ListNode, n int) *ListNode {
    if n <= 0 {
        return head
    }

    dummy := &ListNode{0, head}
    current := dummy

    // 移动到第 n 个结点的前一个结点
    for i := 0; i < n && current != nil; i++ {
        current = current.Next
    }

    if current != nil {
        current.Next = nil
    }

    return dummy.Next
}

Q3: 如果链表很长,如何优化内存使用?

A: 使用双指针法是最优的,因为它不需要额外的空间。另外,可以考虑使用尾递归优化(如果语言支持)。

P7 加分项

1. 深度理解:为什么需要哑结点?

哑结点的作用:

  1. 统一处理: 避免单独处理删除头结点的特殊情况
  2. 简化边界条件: 当要删除的是头结点时,普通方法需要特殊处理
  3. 代码简洁: 使用哑结点后,删除操作统一为 prev.next = prev.next.next

没有哑结点的问题:

// 没有哑结点的版本(需要特殊处理删除头结点)
func removeNthFromEndWithoutDummy(head *ListNode, n int) *ListNode {
    length := 0
    current := head
    for current != nil {
        length++
        current = current.Next
    }

    if n == length {
        // 要删除的是头结点,特殊处理
        return head.Next
    }

    pos := length - n
    current = head
    for i := 0; i < pos-1; i++ {
        current = current.Next
    }
    current.Next = current.Next.Next

    return head
}

2. 实战扩展:链表操作的通用技巧

技巧1快慢指针的应用

  • 找中点: fast 移动 2 步slow 移动 1 步
  • 找倒数第 k 个: fast 先移动 k 步
  • 检测环: fast 移动 2 步slow 移动 1 步
// 找链表中点
func findMiddle(head *ListNode) *ListNode {
    slow, fast := head, head
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
    }
    return slow
}

// 检测环
func hasCycle(head *ListNode) bool {
    slow, fast := head, head
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
        if slow == fast {
            return true
        }
    }
    return false
}

技巧2虚拟头结点的使用

  • 统一操作: 避免边界条件判断
  • 简化代码: 使删除、插入操作更简洁
  • 常见场景: 删除操作、插入操作

3. 变形题目

变形1删除链表中的重复元素

LeetCode 83: 删除排序链表中的重复元素,使得每个元素只出现一次。

func deleteDuplicates(head *ListNode) *ListNode {
    if head == nil {
        return nil
    }

    current := head
    for current.Next != nil {
        if current.Val == current.Next.Val {
            current.Next = current.Next.Next
        } else {
            current = current.Next
        }
    }

    return head
}

变形2删除链表中的所有重复元素

LeetCode 82: 删除排序链表中所有重复的元素,只保留原始链表中没有重复出现的数字。

func deleteDuplicatesAll(head *ListNode) *ListNode {
    dummy := &ListNode{0, head}
    prev := dummy

    for prev.Next != nil {
        curr := prev.Next
        // 检查是否有重复
        if curr.Next != nil && curr.Val == curr.Next.Val {
            // 跳过所有重复的值
            val := curr.Val
            for curr != nil && curr.Val == val {
                curr = curr.Next
            }
            prev.Next = curr
        } else {
            prev = prev.Next
        }
    }

    return dummy.Next
}

变形3旋转链表

LeetCode 61: 将链表每个节点向右移动 k 个位置。

func rotateRight(head *ListNode, k int) *ListNode {
    if head == nil || k == 0 {
        return head
    }

    // 计算链表长度并连接成环
    length := 1
    tail := head
    for tail.Next != nil {
        tail = tail.Next
        length++
    }
    tail.Next = head

    // 计算新的尾结点位置
    k = k % length
    stepsToNewTail := length - k
    newTail := head
    for i := 1; i < stepsToNewTail; i++ {
        newTail = newTail.Next
    }

    newHead := newTail.Next
    newTail.Next = nil

    return newHead
}

4. 优化技巧

优化1一次遍历删除多个结点

如果需要删除多个位置的结点,可以在一次遍历中完成。

func removeNodes(head *ListNode, positions []int) *ListNode {
    dummy := &ListNode{0, head}
    posMap := make(map[int]bool)
    for _, pos := range positions {
        posMap[pos] = true
    }

    prev := dummy
    curr := head
    index := 1

    for curr != nil {
        if posMap[index] {
            prev.Next = curr.Next
        } else {
            prev = curr
        }
        curr = curr.Next
        index++
    }

    return dummy.Next
}

优化2递归解法优雅但可能栈溢出

func removeNthFromEndRecursive(head *ListNode, n int) *ListNode {
    counter := 0
    return removeHelper(head, &counter, n)
}

func removeHelper(node *ListNode, counter *int, n int) *ListNode {
    if node == nil {
        return nil
    }

    node.Next = removeHelper(node.Next, counter, n)
    *counter++

    if *counter == n {
        return node.Next
    }

    return node
}

5. 实际应用场景

  • LRU 缓存: 删除最近最少使用的数据
  • 浏览器历史记录: 删除特定位置的历史记录
  • 文本编辑器: 撤销操作(删除最近的修改)
  • 任务队列: 删除超时或取消的任务

6. 面试技巧

面试官可能会问:

  1. "为什么选择双指针法而不是计算长度法?"
  2. "如果链表很长,递归解法会有什么问题?"
  3. "如何证明你的算法是正确的?"

回答要点:

  1. 双指针法只需一次遍历,代码简洁,空间复杂度低
  2. 递归可能导致栈溢出,对于长链表不推荐
  3. 可以通过画图、举例、边界条件分析来证明正确性

7. 相关题目推荐

  • LeetCode 19: 删除链表的倒数第 N 个结点(本题)
  • LeetCode 61: 旋转链表
  • LeetCode 83: 删除排序链表中的重复元素
  • LeetCode 82: 删除排序链表中的所有重复元素
  • LeetCode 206: 反转链表
  • LeetCode 142: 环形链表 II